Skip to content

Commit 0270c0e

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

File tree

4 files changed

+561
-26
lines changed

4 files changed

+561
-26
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: 280 additions & 8 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,45 @@
4345
import io.trino.spi.connector.ColumnPosition;
4446
import io.trino.spi.connector.ConnectorSession;
4547
import io.trino.spi.connector.ConnectorTableMetadata;
46-
import io.trino.spi.type.Type;
48+
import io.trino.spi.type.*;
4749

4850
import java.sql.Connection;
4951
import java.sql.Date;
52+
import java.sql.PreparedStatement;
53+
import java.sql.SQLException;
54+
import java.sql.Timestamp;
5055
import java.sql.Types;
51-
import java.time.LocalDate;
52-
import java.util.HexFormat;
53-
import java.util.List;
54-
import java.util.Map;
55-
import java.util.Optional;
56-
import java.util.OptionalLong;
57-
import java.util.Set;
56+
import java.time.*;
57+
import java.util.*;
5858
import java.util.function.BiFunction;
5959

60+
import static com.google.common.base.Preconditions.checkArgument;
61+
import static io.trino.plugin.jdbc.PredicatePushdownController.FULL_PUSHDOWN;
6062
import static io.trino.plugin.jdbc.StandardColumnMappings.bigintColumnMapping;
6163
import static io.trino.plugin.jdbc.StandardColumnMappings.booleanColumnMapping;
6264
import static io.trino.plugin.jdbc.StandardColumnMappings.decimalColumnMapping;
6365
import static io.trino.plugin.jdbc.StandardColumnMappings.defaultCharColumnMapping;
6466
import static io.trino.plugin.jdbc.StandardColumnMappings.defaultVarcharColumnMapping;
6567
import static io.trino.plugin.jdbc.StandardColumnMappings.doubleColumnMapping;
68+
import static io.trino.plugin.jdbc.StandardColumnMappings.fromLongTrinoTimestamp;
69+
import static io.trino.plugin.jdbc.StandardColumnMappings.fromTrinoTimestamp;
6670
import static io.trino.plugin.jdbc.StandardColumnMappings.integerColumnMapping;
6771
import static io.trino.plugin.jdbc.StandardColumnMappings.smallintColumnMapping;
72+
import static io.trino.plugin.jdbc.StandardColumnMappings.toLongTrinoTimestamp;
73+
import static io.trino.plugin.jdbc.StandardColumnMappings.toTrinoTimestamp;
6874
import static io.trino.plugin.jdbc.TypeHandlingJdbcSessionProperties.getUnsupportedTypeHandling;
6975
import static io.trino.plugin.jdbc.UnsupportedTypeHandling.CONVERT_TO_VARCHAR;
7076
import static io.trino.spi.StandardErrorCode.NOT_SUPPORTED;
7177
import static io.trino.spi.connector.ConnectorMetadata.MODIFYING_ROWS_MESSAGE;
78+
import static io.trino.spi.type.DateTimeEncoding.packDateTimeWithZone;
79+
import static io.trino.spi.type.DateTimeEncoding.unpackMillisUtc;
7280
import static io.trino.spi.type.DateType.DATE;
7381
import static io.trino.spi.type.DecimalType.createDecimalType;
82+
import static io.trino.spi.type.TimeZoneKey.UTC_KEY;
83+
import static io.trino.spi.type.TimestampType.createTimestampType;
84+
import static io.trino.spi.type.TimestampWithTimeZoneType.createTimestampWithTimeZoneType;
85+
import static io.trino.spi.type.Timestamps.NANOSECONDS_PER_MILLISECOND;
86+
import static io.trino.spi.type.Timestamps.PICOSECONDS_PER_NANOSECOND;
7487
import static io.trino.spi.type.VarbinaryType.VARBINARY;
7588
import static java.lang.String.format;
7689
import static java.util.Locale.ENGLISH;
@@ -79,11 +92,15 @@
7992
public class ExasolClient
8093
extends BaseJdbcClient
8194
{
95+
private static final int EXASOL_TIMESTAMP_WITH_TIMEZONE = 124;
96+
8297
private static final Set<String> INTERNAL_SCHEMAS = ImmutableSet.<String>builder()
8398
.add("EXA_STATISTICS")
8499
.add("SYS")
85100
.build();
86101

102+
private static final int MAX_EXASOL_TIMESTAMP_PRECISION = 9;
103+
87104
@Inject
88105
public ExasolClient(
89106
BaseJdbcConfig config,
@@ -241,6 +258,10 @@ public Optional<ColumnMapping> toColumnMapping(ConnectorSession session, Connect
241258
return Optional.of(defaultVarcharColumnMapping(typeHandle.requiredColumnSize(), true));
242259
case Types.DATE:
243260
return Optional.of(dateColumnMapping());
261+
case Types.TIMESTAMP:
262+
return Optional.of(timestampColumnMapping(typeHandle));
263+
case EXASOL_TIMESTAMP_WITH_TIMEZONE:
264+
return Optional.of(timestampWithTimeZoneColumnMapping(typeHandle));
244265
}
245266

246267
if (getUnsupportedTypeHandling(session) == CONVERT_TO_VARCHAR) {
@@ -256,6 +277,257 @@ private boolean isHashType(JdbcTypeHandle typeHandle)
256277
&& typeHandle.jdbcTypeName().get().equalsIgnoreCase("HASHTYPE");
257278
}
258279

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

0 commit comments

Comments
 (0)