Skip to content

Commit 4ff3087

Browse files
committed
Add support for TIMESTAMP types in Exasol connector
1 parent 6c4e09e commit 4ff3087

File tree

3 files changed

+470
-0
lines changed

3 files changed

+470
-0
lines changed

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

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,13 @@ Trino data type mapping:
101101
* - ``DATE``
102102
- ``DATE``
103103
-
104+
* - ``TIMESTAMP(n)``
105+
- ``TIMESTAMP(n)``
106+
- See :ref:`exasol-timestamp-mapping`
107+
* - ``TIMESTAMP(n) WITH LOCAL TIME ZONE``
108+
- ``TIMESTAMP(n) WITH TIME ZONE``
109+
- See :ref:`exasol-timestamp-with-local-time-zone-mapping`
110+
104111
```
105112

106113
No other types are supported.
@@ -127,6 +134,45 @@ If no length is specified, the connector uses 2000000.
127134
Trino's `CHAR(n)` maps to `CHAR(n)` and vice versa if `n` is no greater than 2000.
128135
Exasol does not support longer values.
129136

137+
(exasol-timestamp-mapping)=
138+
### Mapping `TIMESTAMP` Types
139+
140+
Exasol `TIMESTAMP(n)` columns are mapped to Trino's `TIMESTAMP(n)` type
141+
and vice versa, with the following exceptions:
142+
143+
- **No precision specified**:
144+
If the precision is omitted (i.e., the column is defined as `TIMESTAMP`
145+
without `(n)`), Exasol defaults to a precision of 3.
146+
This maps to Trino's `TIMESTAMP(3)`.
147+
148+
- **Precision greater than 9**:
149+
Exasol supports up to 9 digits of fractional seconds.
150+
If the precision in Exasol exceeds 9, an exception is thrown
151+
152+
- **Negative precision**:
153+
Negative values for precision are invalid. If encountered,
154+
an exception is thrown.
155+
156+
(exasol-timestamp-with-local-time-zone-mapping)=
157+
### Mapping `TIMESTAMP WITH LOCAL TIME ZONE` Types
158+
159+
Exasol `TIMESTAMP(n) WITH LOCAL TIME ZONE` columns are mapped
160+
to Trino's `TIMESTAMP(n) WITH TIME ZONE` type and vice versa,
161+
with the following exceptions:
162+
163+
- **No precision specified**:
164+
If the precision is omitted (i.e., the column is defined
165+
as `TIMESTAMP WITH LOCAL TIME ZONE` without `(n)`), Exasol defaults
166+
to a precision of 3. This maps to Trino's `TIMESTAMP(3) WITH TIME ZONE`.
167+
168+
- **Precision greater than 9**:
169+
Exasol supports up to 9 digits of fractional seconds.
170+
If the precision in Exasol exceeds 9, an exception is thrown.
171+
172+
- **Negative precision**:
173+
Negative values for precision are invalid.
174+
If encountered, an exception is thrown.
175+
130176
```{include} jdbc-type-mapping.fragment
131177
```
132178

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

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import io.trino.plugin.jdbc.JdbcTypeHandle;
2929
import io.trino.plugin.jdbc.LongReadFunction;
3030
import io.trino.plugin.jdbc.LongWriteFunction;
31+
import io.trino.plugin.jdbc.ObjectReadFunction;
32+
import io.trino.plugin.jdbc.ObjectWriteFunction;
3133
import io.trino.plugin.jdbc.QueryBuilder;
3234
import io.trino.plugin.jdbc.WriteFunction;
3335
import io.trino.plugin.jdbc.WriteMapping;
@@ -39,42 +41,68 @@
3941
import io.trino.spi.connector.ColumnPosition;
4042
import io.trino.spi.connector.ConnectorSession;
4143
import io.trino.spi.connector.ConnectorTableMetadata;
44+
import io.trino.spi.type.LongTimestamp;
45+
import io.trino.spi.type.LongTimestampWithTimeZone;
46+
import io.trino.spi.type.TimestampType;
47+
import io.trino.spi.type.TimestampWithTimeZoneType;
4248
import io.trino.spi.type.Type;
4349

4450
import java.sql.Connection;
4551
import java.sql.Date;
52+
import java.sql.PreparedStatement;
53+
import java.sql.SQLException;
54+
import java.sql.Timestamp;
4655
import java.sql.Types;
56+
import java.time.Instant;
4757
import java.time.LocalDate;
58+
import java.time.LocalDateTime;
4859
import java.util.List;
4960
import java.util.Map;
5061
import java.util.Optional;
5162
import java.util.OptionalLong;
5263
import java.util.Set;
5364

65+
import static com.google.common.base.Preconditions.checkArgument;
66+
import static io.trino.plugin.jdbc.PredicatePushdownController.FULL_PUSHDOWN;
5467
import static io.trino.plugin.jdbc.StandardColumnMappings.bigintColumnMapping;
5568
import static io.trino.plugin.jdbc.StandardColumnMappings.booleanColumnMapping;
5669
import static io.trino.plugin.jdbc.StandardColumnMappings.decimalColumnMapping;
5770
import static io.trino.plugin.jdbc.StandardColumnMappings.defaultCharColumnMapping;
5871
import static io.trino.plugin.jdbc.StandardColumnMappings.defaultVarcharColumnMapping;
5972
import static io.trino.plugin.jdbc.StandardColumnMappings.doubleColumnMapping;
73+
import static io.trino.plugin.jdbc.StandardColumnMappings.fromLongTrinoTimestamp;
74+
import static io.trino.plugin.jdbc.StandardColumnMappings.fromTrinoTimestamp;
6075
import static io.trino.plugin.jdbc.StandardColumnMappings.integerColumnMapping;
6176
import static io.trino.plugin.jdbc.StandardColumnMappings.smallintColumnMapping;
77+
import static io.trino.plugin.jdbc.StandardColumnMappings.toLongTrinoTimestamp;
78+
import static io.trino.plugin.jdbc.StandardColumnMappings.toTrinoTimestamp;
6279
import static io.trino.plugin.jdbc.TypeHandlingJdbcSessionProperties.getUnsupportedTypeHandling;
6380
import static io.trino.plugin.jdbc.UnsupportedTypeHandling.CONVERT_TO_VARCHAR;
6481
import static io.trino.spi.StandardErrorCode.NOT_SUPPORTED;
6582
import static io.trino.spi.connector.ConnectorMetadata.MODIFYING_ROWS_MESSAGE;
83+
import static io.trino.spi.type.DateTimeEncoding.packDateTimeWithZone;
6684
import static io.trino.spi.type.DateType.DATE;
6785
import static io.trino.spi.type.DecimalType.createDecimalType;
86+
import static io.trino.spi.type.TimeZoneKey.UTC_KEY;
87+
import static io.trino.spi.type.TimestampType.createTimestampType;
88+
import static io.trino.spi.type.TimestampWithTimeZoneType.createTimestampWithTimeZoneType;
89+
import static io.trino.spi.type.Timestamps.NANOSECONDS_PER_MILLISECOND;
90+
import static io.trino.spi.type.Timestamps.PICOSECONDS_PER_NANOSECOND;
91+
import static java.lang.String.format;
6892
import static java.util.Locale.ENGLISH;
6993

7094
public class ExasolClient
7195
extends BaseJdbcClient
7296
{
97+
private static final int EXASOL_TIMESTAMP_WITH_TIMEZONE = 124;
98+
7399
private static final Set<String> INTERNAL_SCHEMAS = ImmutableSet.<String>builder()
74100
.add("EXA_STATISTICS")
75101
.add("SYS")
76102
.build();
77103

104+
private static final int MAX_EXASOL_TIMESTAMP_PRECISION = 9;
105+
78106
@Inject
79107
public ExasolClient(
80108
BaseJdbcConfig config,
@@ -229,6 +257,10 @@ public Optional<ColumnMapping> toColumnMapping(ConnectorSession session, Connect
229257
return Optional.of(defaultVarcharColumnMapping(typeHandle.requiredColumnSize(), true));
230258
case Types.DATE:
231259
return Optional.of(dateColumnMapping());
260+
case Types.TIMESTAMP:
261+
return Optional.of(timestampColumnMapping(typeHandle));
262+
case EXASOL_TIMESTAMP_WITH_TIMEZONE:
263+
return Optional.of(timestampWithTimeZoneColumnMapping(typeHandle));
232264
}
233265

234266
if (getUnsupportedTypeHandling(session) == CONVERT_TO_VARCHAR) {
@@ -237,6 +269,247 @@ public Optional<ColumnMapping> toColumnMapping(ConnectorSession session, Connect
237269
return Optional.empty();
238270
}
239271

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

0 commit comments

Comments
 (0)