diff --git a/docs/src/main/sphinx/connector.md b/docs/src/main/sphinx/connector.md index 6805fdca440a..3c2d75481b28 100644 --- a/docs/src/main/sphinx/connector.md +++ b/docs/src/main/sphinx/connector.md @@ -43,6 +43,7 @@ Snowflake SQL Server System Thrift +Tibero TPC-DS TPC-H ``` diff --git a/docs/src/main/sphinx/connector/tibero.md b/docs/src/main/sphinx/connector/tibero.md new file mode 100644 index 000000000000..f63fcbc7662f --- /dev/null +++ b/docs/src/main/sphinx/connector/tibero.md @@ -0,0 +1,215 @@ +# Tibero connector + +The Tibero connector allows querying and creating tables in an external +[Tibero](https://www.tmaxtibero.com/product/productView.do?prod_cd=tibero&detail_gubun=prod_main) database. +Tibero is an enterprise-grade relational database management system developed +by TmaxSoft. + +## Requirements + +To connect to Tibero, you need: + +- Tibero 6 or higher. +- Tibero JDBC driver (`tibero7-jdbc.jar`) manually copied into the Trino plugin + directory. The driver is not bundled with Trino because it is not available + in public Maven repositories. + +## JDBC driver installation + +The Tibero JDBC driver must be obtained from TmaxSoft and placed in the Trino +plugin directory manually. + +### Obtaining the driver + +- **If you have Tibero database installed**: The JDBC driver is included in the + Tibero installation at `$TB_HOME/client/lib/jar/`. See the + [Tibero JDBC documentation](https://docs.tibero.com/tibero/en/topics/development/jdbc-developers-guide/introduction-to-tibero-jdbc#default-path) + for more details. + +- **If you don't have Tibero installed**: Download the Tibero distribution from + [TechNet](https://technet.tibero.com/en/front/download/findDownloadList.do). + After extracting the archive, the JDBC driver is located at + `$TB_HOME/client/lib/jar/`. + +Contact TmaxSoft for licensing information and to ensure the driver can be used +in your environment. + +### Installing the driver + +Copy the JDBC driver JAR file into the Tibero connector plugin directory: + +```bash +cp /path/to/tibero7-jdbc.jar /plugin/tibero/ +``` + +### Docker deployment + +When using the official `trinodb/trino` Docker image, the Tibero connector +plugin is included but the JDBC driver must be added separately. Create a +custom Docker image as follows: + +```dockerfile +FROM trinodb/trino:{doc_version} + +# Copy the Tibero JDBC driver obtained from TmaxSoft +COPY tibero7-jdbc.jar /usr/lib/trino/plugin/tibero/ + +# Add the Tibero catalog configuration +COPY tibero.properties /etc/trino/catalog/tibero.properties +``` + +Build and run the image: + +```bash +docker build -t trino-tibero . +docker run -p 8080:8080 trino-tibero +``` + +> **Note:** +Ensure you have the appropriate license from TmaxSoft to use the Tibero JDBC +driver in your environment. The driver's usage is subject to TmaxSoft's +licensing terms. + +## Configuration + +To configure the Tibero connector as the `example` catalog, create a file +named `example.properties` in `etc/catalog`. Include the following +connection properties in the file: + +```text +connector.name=tibero +connection-url=jdbc:tibero:thin:@example.net:8629:tibero +connection-user=tibero +connection-password=secret +``` + +The `connection-url` defines the connection information and parameters to pass +to the JDBC driver. The Tibero connector uses the Tibero JDBC Thin driver. +The format is: + +``` +jdbc:tibero:thin:@:: +``` + +The `connection-user` and `connection-password` are typically required and +determine the user credentials for the connection, often a service user. You can +use {doc}`secrets ` to avoid actual values in the catalog +properties files. + + +## Querying Tibero + +The Tibero connector provides a schema for every Tibero database. + +Run `SHOW SCHEMAS` to see the available Tibero databases: + +``` +SHOW SCHEMAS FROM example; +``` + +If you used a different name for your catalog properties file, use that catalog +name instead of `example`. + +> **Note:** +The Tibero user must have access to the table in order to access it from Trino. +The user configuration, in the connection properties file, determines your +privileges in these schemas. + + +### Examples + +If you have a Tibero database named `web`, run `SHOW TABLES` to see the +tables it contains: + +``` +SHOW TABLES FROM example.web; +``` + +To see a list of the columns in the `clicks` table in the `web` +database, run either of the following: + +``` +DESCRIBE example.web.clicks; +SHOW COLUMNS FROM example.web.clicks; +``` + +To access the clicks table in the web database, run the following: + +``` +SELECT * FROM example.web.clicks; +``` + + +## Type mapping + +Because Trino and Tibero each support types that the other does not, this +connector {ref}`modifies some types ` when reading or +writing data. Data types may not map the same way in both directions between +Trino and the data source. Refer to the following sections for type mapping in +each direction. + +### Tibero to Trino type mapping + +Trino supports reading the following Tibero database types: + +| Tibero type | Trino type | Notes | +|-------------|------------|-------| +| `SMALLINT` | `SMALLINT` | | +| `INTEGER` | `INTEGER` | | +| `BIGINT` | `BIGINT` | | +| `REAL` | `REAL` | | +| `DOUBLE` | `DOUBLE` | | +| `NUMBER(p, s)`, `DECIMAL(p, s)` | `DECIMAL(p, s)` | Default precision is 38 if not specified | +| `CHAR(n)` | `CHAR(n)` | | +| `VARCHAR(n)`, `NVARCHAR(n)` | `VARCHAR(n)` | | +| `CLOB`, `NCLOB` | `VARCHAR` | Unbounded VARCHAR | +| `RAW(n)`, `BLOB` | `VARBINARY` | | +| `DATE` | `TIMESTAMP(0)` | Tibero DATE includes time components | +| `TIMESTAMP(p)` | `TIMESTAMP(p)` | Precision up to 9 digits | + +No other types are supported. + +### Trino to Tibero type mapping + +Trino supports creating tables with the following types in Tibero: + +| Trino type | Tibero type | Notes | +|------------|-------------|-------| +| `SMALLINT` | `SMALLINT` | | +| `INTEGER` | `INTEGER` | | +| `BIGINT` | `BIGINT` | | +| `REAL` | `REAL` | | +| `DOUBLE` | `DOUBLE PRECISION` | | +| `CHAR(n)` | `CHAR(n)` | | +| `VARCHAR(n)`, `VARCHAR` | `VARCHAR(n)`, `VARCHAR` | Unbounded VARCHAR is supported | + +No other types are supported. + + +### Mapping datetime types + +Writing a timestamp with fractional second precision (`p`) greater than 9 +rounds the fractional seconds to nine digits. + +Tibero `DATE` type stores year, month, day, hour, minute, and seconds, so it +is mapped to Trino `TIMESTAMP(0)`. + +```{include} jdbc-type-mapping.fragment +``` + + +## SQL support + +The connector provides read access and write access to data and metadata in +Tibero. In addition to the [globally available](sql-globally-available) and +[read operation](sql-read-operations) statements, the connector supports the +following features: + +- [](/sql/insert) +- [](/sql/delete) +- [](/sql/truncate) +- [](/sql/create-table) +- [](/sql/create-table-as) +- [](/sql/drop-table) +- [](/sql/alter-table) +- [](/sql/comment) diff --git a/plugin/trino-tibero/pom.xml b/plugin/trino-tibero/pom.xml new file mode 100644 index 000000000000..7f515085d4f2 --- /dev/null +++ b/plugin/trino-tibero/pom.xml @@ -0,0 +1,122 @@ + + + 4.0.0 + + + io.trino + trino-root + 481-SNAPSHOT + ../../pom.xml + + + trino-tibero + trino-plugin + ${project.artifactId} + Trino - Tibero connector + + + + com.google.guava + guava + + + + com.google.inject + guice + classes + + + + io.airlift + configuration + + + + io.trino + trino-base-jdbc + + + + io.trino + trino-plugin-toolkit + + + + com.fasterxml.jackson.core + jackson-annotations + provided + + + + io.airlift + slice + provided + + + + io.opentelemetry + opentelemetry-api + provided + + + + io.opentelemetry + opentelemetry-api-incubator + provided + + + + io.opentelemetry + opentelemetry-context + provided + + + + io.trino + trino-spi + provided + + + + io.airlift + junit-extensions + test + + + + io.airlift + testing + test + + + + io.trino + trino-main + test + + + + io.trino + trino-testing + test + + + + org.assertj + assertj-core + test + + + + org.junit.jupiter + junit-jupiter-api + test + + + + org.junit.jupiter + junit-jupiter-engine + test + + + diff --git a/plugin/trino-tibero/src/main/java/io/trino/plugin/tibero/TiberoClient.java b/plugin/trino-tibero/src/main/java/io/trino/plugin/tibero/TiberoClient.java new file mode 100644 index 000000000000..7616797e0c31 --- /dev/null +++ b/plugin/trino-tibero/src/main/java/io/trino/plugin/tibero/TiberoClient.java @@ -0,0 +1,431 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.plugin.tibero; + +import com.google.inject.Inject; +import io.trino.plugin.base.mapping.IdentifierMapping; +import io.trino.plugin.jdbc.BaseJdbcClient; +import io.trino.plugin.jdbc.BaseJdbcConfig; +import io.trino.plugin.jdbc.ColumnMapping; +import io.trino.plugin.jdbc.ConnectionFactory; +import io.trino.plugin.jdbc.JdbcTypeHandle; +import io.trino.plugin.jdbc.LongReadFunction; +import io.trino.plugin.jdbc.LongWriteFunction; +import io.trino.plugin.jdbc.ObjectReadFunction; +import io.trino.plugin.jdbc.ObjectWriteFunction; +import io.trino.plugin.jdbc.QueryBuilder; +import io.trino.plugin.jdbc.WriteMapping; +import io.trino.plugin.jdbc.logging.RemoteQueryModifier; +import io.trino.spi.TrinoException; +import io.trino.spi.connector.ConnectorSession; +import io.trino.spi.type.CharType; +import io.trino.spi.type.DecimalType; +import io.trino.spi.type.LongTimestamp; +import io.trino.spi.type.TimestampType; +import io.trino.spi.type.Type; +import io.trino.spi.type.VarcharType; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.sql.Types; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.temporal.ChronoField; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Verify.verify; +import static io.airlift.slice.Slices.utf8Slice; +import static io.airlift.slice.Slices.wrappedBuffer; +import static io.trino.plugin.jdbc.JdbcErrorCode.JDBC_ERROR; +import static io.trino.plugin.jdbc.PredicatePushdownController.DISABLE_PUSHDOWN; +import static io.trino.plugin.jdbc.PredicatePushdownController.FULL_PUSHDOWN; +import static io.trino.plugin.jdbc.StandardColumnMappings.bigintColumnMapping; +import static io.trino.plugin.jdbc.StandardColumnMappings.bigintWriteFunction; +import static io.trino.plugin.jdbc.StandardColumnMappings.charReadFunction; +import static io.trino.plugin.jdbc.StandardColumnMappings.charWriteFunction; +import static io.trino.plugin.jdbc.StandardColumnMappings.decimalColumnMapping; +import static io.trino.plugin.jdbc.StandardColumnMappings.doubleColumnMapping; +import static io.trino.plugin.jdbc.StandardColumnMappings.doubleWriteFunction; +import static io.trino.plugin.jdbc.StandardColumnMappings.fromLongTrinoTimestamp; +import static io.trino.plugin.jdbc.StandardColumnMappings.fromTrinoTimestamp; +import static io.trino.plugin.jdbc.StandardColumnMappings.integerColumnMapping; +import static io.trino.plugin.jdbc.StandardColumnMappings.integerWriteFunction; +import static io.trino.plugin.jdbc.StandardColumnMappings.realColumnMapping; +import static io.trino.plugin.jdbc.StandardColumnMappings.realWriteFunction; +import static io.trino.plugin.jdbc.StandardColumnMappings.smallintColumnMapping; +import static io.trino.plugin.jdbc.StandardColumnMappings.smallintWriteFunction; +import static io.trino.plugin.jdbc.StandardColumnMappings.toLongTrinoTimestamp; +import static io.trino.plugin.jdbc.StandardColumnMappings.toTrinoTimestamp; +import static io.trino.plugin.jdbc.StandardColumnMappings.varbinaryWriteFunction; +import static io.trino.plugin.jdbc.StandardColumnMappings.varcharReadFunction; +import static io.trino.plugin.jdbc.StandardColumnMappings.varcharWriteFunction; +import static io.trino.plugin.jdbc.TypeHandlingJdbcSessionProperties.getUnsupportedTypeHandling; +import static io.trino.plugin.jdbc.UnsupportedTypeHandling.CONVERT_TO_VARCHAR; +import static io.trino.spi.StandardErrorCode.NOT_SUPPORTED; +import static io.trino.spi.type.BigintType.BIGINT; +import static io.trino.spi.type.CharType.createCharType; +import static io.trino.spi.type.DoubleType.DOUBLE; +import static io.trino.spi.type.IntegerType.INTEGER; +import static io.trino.spi.type.RealType.REAL; +import static io.trino.spi.type.SmallintType.SMALLINT; +import static io.trino.spi.type.TimestampType.MAX_SHORT_PRECISION; +import static io.trino.spi.type.TimestampType.TIMESTAMP_SECONDS; +import static io.trino.spi.type.TimestampType.createTimestampType; +import static io.trino.spi.type.Timestamps.MICROSECONDS_PER_SECOND; +import static io.trino.spi.type.VarbinaryType.VARBINARY; +import static io.trino.spi.type.VarcharType.createUnboundedVarcharType; +import static io.trino.spi.type.VarcharType.createVarcharType; +import static java.lang.Math.floorDiv; +import static java.lang.Math.floorMod; +import static java.lang.String.format; + +public class TiberoClient + extends BaseJdbcClient +{ + private static final int MAX_TIBERO_TIMESTAMP_PRECISION = 9; + + private static final DateTimeFormatter TIMESTAMP_SECONDS_FORMATTER = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss"); + + private static final DateTimeFormatter TIMESTAMP_NANO_OPTIONAL_FORMATTER = new DateTimeFormatterBuilder() + .appendPattern("uuuu-MM-dd HH:mm:ss") + .optionalStart() + .appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true) + .optionalEnd() + .toFormatter(); + + @Inject + public TiberoClient( + BaseJdbcConfig config, + ConnectionFactory connectionFactory, + QueryBuilder queryBuilder, + IdentifierMapping identifierMapping, + RemoteQueryModifier remoteQueryModifier) + { + super("\"", connectionFactory, queryBuilder, config.getJdbcTypesMappedToVarchar(), identifierMapping, remoteQueryModifier, true); + } + + @Override + public Optional toColumnMapping(ConnectorSession session, Connection connection, JdbcTypeHandle typeHandle) + { + Optional mapping = getForcedMappingToVarchar(typeHandle); + if (mapping.isPresent()) { + return mapping; + } + + String jdbcTypeName = typeHandle.jdbcTypeName() + .orElseThrow(() -> new TrinoException(JDBC_ERROR, "Type name is missing: " + typeHandle)); + + if (jdbcTypeName.equalsIgnoreCase("date")) { + return Optional.of(ColumnMapping.longMapping( + TIMESTAMP_SECONDS, + tiberoTimestampReadFunction(TIMESTAMP_SECONDS), + trinoTimestampToTiberoDateWriteFunction(), + FULL_PUSHDOWN)); + } + switch (typeHandle.jdbcType()) { + case Types.SMALLINT: + return Optional.of(smallintColumnMapping()); + + case Types.INTEGER: + return Optional.of(integerColumnMapping()); + + case Types.BIGINT: + return Optional.of(bigintColumnMapping()); + + case Types.REAL: + return Optional.of(realColumnMapping()); + + case Types.DOUBLE: + return Optional.of(doubleColumnMapping()); + + case Types.CHAR: + return Optional.of(charColumnMapping(typeHandle.requiredColumnSize())); + + case Types.NUMERIC: + case Types.DECIMAL: + int precision = typeHandle.columnSize().orElse(38); + int scale = typeHandle.decimalDigits().orElse(0); + DecimalType decimalType = DecimalType.createDecimalType(precision, scale); + return Optional.of(decimalColumnMapping(decimalType)); + + case Types.TIMESTAMP: + int timestampPrecision = typeHandle.requiredDecimalDigits(); + return Optional.of(tiberoTimestampColumnMapping(createTimestampType(timestampPrecision))); + + case Types.CLOB: + case Types.NCLOB: + return Optional.of(ColumnMapping.sliceMapping( + createUnboundedVarcharType(), + (resultSet, columnIndex) -> utf8Slice(resultSet.getString(columnIndex)), + varcharWriteFunction(), + DISABLE_PUSHDOWN)); + + case Types.VARBINARY: // Tibero's RAW(n) + case Types.BLOB: + return Optional.of(ColumnMapping.sliceMapping( + VARBINARY, + (resultSet, columnIndex) -> wrappedBuffer(resultSet.getBytes(columnIndex)), + varbinaryWriteFunction(), + DISABLE_PUSHDOWN)); + + case Types.VARCHAR: + case Types.NVARCHAR: + return Optional.of(ColumnMapping.sliceMapping( + createVarcharType(typeHandle.requiredColumnSize()), + (varcharResultSet, varcharColumnIndex) -> utf8Slice(varcharResultSet.getString(varcharColumnIndex)), + varcharWriteFunction(), + FULL_PUSHDOWN)); + } + + if (getUnsupportedTypeHandling(session) == CONVERT_TO_VARCHAR) { + return mapToUnboundedVarchar(typeHandle); + } + + return Optional.empty(); + } + + private static ColumnMapping tiberoTimestampColumnMapping(TimestampType timestampType) + { + if (timestampType.isShort()) { + return ColumnMapping.longMapping( + timestampType, + tiberoTimestampReadFunction(timestampType), + tiberoTimestampWriteFunction(timestampType), + FULL_PUSHDOWN); + } + return ColumnMapping.objectMapping( + timestampType, + tiberoLongTimestampReadFunction(timestampType), + tiberoLongTimestampWriteFunction(timestampType), + FULL_PUSHDOWN); + } + + private static ObjectReadFunction tiberoLongTimestampReadFunction(TimestampType timestampType) + { + verifyLongTimestampPrecision(timestampType); + return ObjectReadFunction.of( + LongTimestamp.class, + (resultSet, columnIndex) -> { + LocalDateTime timestamp = resultSet.getObject(columnIndex, LocalDateTime.class); + // Adjust years when the value is B.C. dates because Oracle returns +1 year unless converting to string in their server side + if (timestamp.getYear() <= 0) { + timestamp = timestamp.minusYears(1); + } + return toLongTrinoTimestamp(timestampType, timestamp); + }); + } + + private static ObjectWriteFunction tiberoLongTimestampWriteFunction(TimestampType timestampType) + { + int precision = timestampType.getPrecision(); + verifyLongTimestampPrecision(timestampType); + + return new ObjectWriteFunction() { + @Override + public Class getJavaType() + { + return LongTimestamp.class; + } + + @Override + public void set(PreparedStatement statement, int index, Object value) + throws SQLException + { + LocalDateTime timestamp = fromLongTrinoTimestamp((LongTimestamp) value, precision); + statement.setString(index, TIMESTAMP_NANO_OPTIONAL_FORMATTER.format(timestamp)); + } + + @Override + public String getBindExpression() + { + return getTiberoBindExpression(precision); + } + + @Override + public void setNull(PreparedStatement statement, int index) + throws SQLException + { + statement.setNull(index, Types.VARCHAR); + } + }; + } + + private static void verifyLongTimestampPrecision(TimestampType timestampType) + { + int precision = timestampType.getPrecision(); + checkArgument(precision > MAX_SHORT_PRECISION && precision <= MAX_TIBERO_TIMESTAMP_PRECISION, + "Precision is out of range: %s", precision); + } + + private static LongWriteFunction trinoTimestampToTiberoDateWriteFunction() + { + return new LongWriteFunction() + { + @Override + public String getBindExpression() + { + // Oracle's DATE stores year, month, day, hour, minute, seconds, but not second fraction + return "TO_DATE(?, 'SYYYY-MM-DD HH24:MI:SS')"; + } + + @Override + public void set(PreparedStatement statement, int index, long value) + throws SQLException + { + long epochSecond = floorDiv(value, MICROSECONDS_PER_SECOND); + int microsOfSecond = floorMod(value, MICROSECONDS_PER_SECOND); + verify(microsOfSecond == 0, "Micros of second must be zero: '%s'", value); + LocalDateTime localDateTime = LocalDateTime.ofEpochSecond(epochSecond, 0, ZoneOffset.UTC); + statement.setString(index, TIMESTAMP_SECONDS_FORMATTER.format(localDateTime)); + } + + @Override + public void setNull(PreparedStatement statement, int index) + throws SQLException + { + statement.setNull(index, Types.VARCHAR); + } + }; + } + + private static LongReadFunction tiberoTimestampReadFunction(TimestampType timestampType) + { + return (resultSet, columnIndex) -> { + Timestamp ts = resultSet.getTimestamp(columnIndex); + + // LocalDateTime timestamp = resultSet.getObject(columnIndex, LocalDateTime.class); + LocalDateTime timestamp = ts.toLocalDateTime(); + + // Adjust years when the value is B.C. dates because Oracle returns +1 year unless converting to string in their server side + if (timestamp.getYear() <= 0) { + timestamp = timestamp.minusYears(1); + } + return toTrinoTimestamp(timestampType, timestamp); + }; + } + + private static LongWriteFunction tiberoTimestampWriteFunction(TimestampType timestampType) + { + return new LongWriteFunction() + { + @Override + public String getBindExpression() + { + return getTiberoBindExpression(timestampType.getPrecision()); + } + + @Override + public void set(PreparedStatement statement, int index, long epochMicros) + throws SQLException + { + LocalDateTime timestamp = fromTrinoTimestamp(epochMicros); + statement.setString(index, TIMESTAMP_NANO_OPTIONAL_FORMATTER.format(timestamp)); + } + + @Override + public void setNull(PreparedStatement statement, int index) + throws SQLException + { + statement.setNull(index, Types.VARCHAR); + } + }; + } + + private static String getTiberoBindExpression(int precision) + { + if (precision == 0) { + return "TO_TIMESTAMP(?, 'SYYYY-MM-DD HH24:MI:SS')"; + } + if (precision <= 2) { + return "TO_TIMESTAMP(?, 'SYYYY-MM-DD HH24:MI:SS.FF')"; + } + + return format("TO_TIMESTAMP(?, 'SYYYY-MM-DD HH24:MI:SS.FF%d')", precision); + } + + @Override + public WriteMapping toWriteMapping(ConnectorSession session, Type type) + { + if (type == SMALLINT) { + return WriteMapping.longMapping("smallint", smallintWriteFunction()); + } + if (type == INTEGER) { + return WriteMapping.longMapping("integer", integerWriteFunction()); + } + if (type == BIGINT) { + return WriteMapping.longMapping("bigint", bigintWriteFunction()); + } + + if (type == REAL) { + return WriteMapping.longMapping("real", realWriteFunction()); + } + if (type == DOUBLE) { + return WriteMapping.doubleMapping("double precision", doubleWriteFunction()); + } + + if (type instanceof CharType charType) { + return WriteMapping.sliceMapping("char(" + charType.getLength() + ")", charWriteFunction()); + } + + if (type instanceof VarcharType varcharType) { + String dataType; + if (varcharType.isUnbounded()) { + dataType = "varchar"; + } + else { + dataType = "varchar(" + varcharType.getBoundedLength() + ")"; + } + return WriteMapping.sliceMapping(dataType, varcharWriteFunction()); + } + + if (type instanceof TimestampType timestampType) { + if (timestampType.isShort()) { + return WriteMapping.longMapping("timestamp(" + timestampType.getPrecision() + ")", tiberoTimestampWriteFunction(timestampType)); + } + return WriteMapping.objectMapping("timestamp(" + timestampType.getPrecision() + ")", tiberoLongTimestampWriteFunction(timestampType)); + } + + throw new TrinoException(NOT_SUPPORTED, "Unsupported column type: " + type.getDisplayName()); + } + + private static ColumnMapping charColumnMapping(int charLength) + { + if (charLength > CharType.MAX_LENGTH) { + return varcharColumnMapping(charLength); + } + CharType charType = createCharType(charLength); + return ColumnMapping.sliceMapping( + charType, + charReadFunction(charType), + charWriteFunction(), + DISABLE_PUSHDOWN); + } + + private static ColumnMapping varcharColumnMapping(int varcharLength) + { + VarcharType varcharType = varcharLength <= VarcharType.MAX_LENGTH + ? createVarcharType(varcharLength) + : createUnboundedVarcharType(); + return ColumnMapping.sliceMapping( + varcharType, + varcharReadFunction(varcharType), + varcharWriteFunction(), + DISABLE_PUSHDOWN); + } +} diff --git a/plugin/trino-tibero/src/main/java/io/trino/plugin/tibero/TiberoClientModule.java b/plugin/trino-tibero/src/main/java/io/trino/plugin/tibero/TiberoClientModule.java new file mode 100644 index 000000000000..45b742ddf3b6 --- /dev/null +++ b/plugin/trino-tibero/src/main/java/io/trino/plugin/tibero/TiberoClientModule.java @@ -0,0 +1,97 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.plugin.tibero; + +import com.google.inject.Binder; +import com.google.inject.Provides; +import com.google.inject.Scopes; +import com.google.inject.Singleton; +import io.airlift.configuration.AbstractConfigurationAwareModule; +import io.opentelemetry.api.OpenTelemetry; +import io.trino.plugin.jdbc.BaseJdbcConfig; +import io.trino.plugin.jdbc.ConnectionFactory; +import io.trino.plugin.jdbc.DriverConnectionFactory; +import io.trino.plugin.jdbc.ForBaseJdbc; +import io.trino.plugin.jdbc.JdbcClient; +import io.trino.plugin.jdbc.credential.CredentialProvider; +import io.trino.spi.connector.ConnectorSession; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.Properties; + +public class TiberoClientModule + extends AbstractConfigurationAwareModule +{ + @Override + public void setup(Binder binder) + { + binder.bind(JdbcClient.class).annotatedWith(ForBaseJdbc.class).to(TiberoClient.class).in(Scopes.SINGLETON); + } + + @Provides + @Singleton + @ForBaseJdbc + public static ConnectionFactory getConnectionFactory(BaseJdbcConfig config, CredentialProvider credentialProvider, OpenTelemetry openTelemetry) + { + // The Tibero JDBC driver (tibero7-jdbc.jar) is not available in public Maven repositories + // and cannot be bundled with the connector. Users must obtain the driver from TmaxSoft + // and place it in the plugin directory manually (see connector documentation). + // + // Driver loading is deferred to the first actual connection attempt to allow the connector + // to initialize successfully even when the driver is not present at startup time. + // This enables plugin loading and configuration validation without requiring the driver JAR. + String connectionUrl = config.getConnectionUrl(); + return new ConnectionFactory() + { + private volatile ConnectionFactory delegate; + + @Override + public Connection openConnection(ConnectorSession session) + throws SQLException + { + return getDelegate().openConnection(session); + } + + @Override + public void close() + throws SQLException + { + if (delegate != null) { + delegate.close(); + } + } + + private ConnectionFactory getDelegate() + throws SQLException + { + if (delegate == null) { + synchronized (this) { + if (delegate == null) { + delegate = DriverConnectionFactory.builder( + DriverManager.getDriver(connectionUrl), + connectionUrl, + credentialProvider) + .setConnectionProperties(new Properties()) + .setOpenTelemetry(openTelemetry) + .build(); + } + } + } + return delegate; + } + }; + } +} diff --git a/plugin/trino-tibero/src/main/java/io/trino/plugin/tibero/TiberoPlugin.java b/plugin/trino-tibero/src/main/java/io/trino/plugin/tibero/TiberoPlugin.java new file mode 100644 index 000000000000..eeb2bb6c16ed --- /dev/null +++ b/plugin/trino-tibero/src/main/java/io/trino/plugin/tibero/TiberoPlugin.java @@ -0,0 +1,25 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.plugin.tibero; + +import io.trino.plugin.jdbc.JdbcPlugin; + +public class TiberoPlugin + extends JdbcPlugin +{ + public TiberoPlugin() + { + super("tibero", TiberoClientModule::new); + } +} diff --git a/plugin/trino-tibero/src/test/java/io/trino/plugin/tibero/TestTiberoClient.java b/plugin/trino-tibero/src/test/java/io/trino/plugin/tibero/TestTiberoClient.java new file mode 100644 index 000000000000..90e4271a0c2f --- /dev/null +++ b/plugin/trino-tibero/src/test/java/io/trino/plugin/tibero/TestTiberoClient.java @@ -0,0 +1,122 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.plugin.tibero; + +import io.trino.plugin.base.mapping.DefaultIdentifierMapping; +import io.trino.plugin.jdbc.BaseJdbcConfig; +import io.trino.plugin.jdbc.DefaultQueryBuilder; +import io.trino.plugin.jdbc.JdbcClient; +import io.trino.plugin.jdbc.WriteFunction; +import io.trino.plugin.jdbc.WriteMapping; +import io.trino.plugin.jdbc.logging.RemoteQueryModifier; +import io.trino.spi.connector.ConnectorSession; +import io.trino.spi.type.Type; +import io.trino.testing.TestingConnectorSession; +import org.junit.jupiter.api.Test; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Types; + +import static com.google.common.reflect.Reflection.newProxy; +import static io.trino.spi.type.BigintType.BIGINT; +import static io.trino.spi.type.CharType.createCharType; +import static io.trino.spi.type.DoubleType.DOUBLE; +import static io.trino.spi.type.IntegerType.INTEGER; +import static io.trino.spi.type.RealType.REAL; +import static io.trino.spi.type.SmallintType.SMALLINT; +import static io.trino.spi.type.TimestampType.TIMESTAMP_MICROS; +import static io.trino.spi.type.TimestampType.TIMESTAMP_MILLIS; +import static io.trino.spi.type.TimestampType.TIMESTAMP_NANOS; +import static io.trino.spi.type.TimestampType.TIMESTAMP_SECONDS; +import static io.trino.spi.type.VarcharType.createUnboundedVarcharType; +import static io.trino.spi.type.VarcharType.createVarcharType; +import static org.assertj.core.api.Assertions.assertThat; + +public class TestTiberoClient +{ + private static final JdbcClient CLIENT = new TiberoClient( + new BaseJdbcConfig(), + session -> { + throw new UnsupportedOperationException(); + }, + new DefaultQueryBuilder(RemoteQueryModifier.NONE), + new DefaultIdentifierMapping(), + RemoteQueryModifier.NONE); + + private static final ConnectorSession SESSION = TestingConnectorSession.SESSION; + + @Test + public void testTypedNullWriteMapping() + throws SQLException + { + testTypedNullWriteMapping(SMALLINT, "smallint", Types.SMALLINT); + testTypedNullWriteMapping(INTEGER, "integer", Types.INTEGER); + testTypedNullWriteMapping(BIGINT, "bigint", Types.BIGINT); + testTypedNullWriteMapping(REAL, "real", Types.REAL); + testTypedNullWriteMapping(DOUBLE, "double precision", Types.DOUBLE); + testTypedNullWriteMapping(createCharType(25), "char(25)", Types.CHAR); + testTypedNullWriteMapping(createUnboundedVarcharType(), "varchar", Types.VARCHAR); + testTypedNullWriteMapping(createVarcharType(123), "varchar(123)", Types.VARCHAR); + } + + @Test + public void testTimestampWriteMapping() + throws SQLException + { + testTimestampWriteMapping(TIMESTAMP_SECONDS, "TO_TIMESTAMP(?, 'SYYYY-MM-DD HH24:MI:SS')", Types.VARCHAR); + testTimestampWriteMapping(TIMESTAMP_MILLIS, "TO_TIMESTAMP(?, 'SYYYY-MM-DD HH24:MI:SS.FF3')", Types.VARCHAR); + testTimestampWriteMapping(TIMESTAMP_MICROS, "TO_TIMESTAMP(?, 'SYYYY-MM-DD HH24:MI:SS.FF6')", Types.VARCHAR); + testTimestampWriteMapping(TIMESTAMP_NANOS, "TO_TIMESTAMP(?, 'SYYYY-MM-DD HH24:MI:SS.FF9')", Types.VARCHAR); + } + + private void testTypedNullWriteMapping(Type type, String dataType, int nullJdbcType) + throws SQLException + { + WriteMapping writeMapping = CLIENT.toWriteMapping(SESSION, type); + assertThat(writeMapping.getWriteFunction()).isNotNull(); + assertThat(writeMapping.getDataType()).isEqualTo(dataType); + + WriteFunction writeFunction = writeMapping.getWriteFunction(); + PreparedStatement statement = newProxy(PreparedStatement.class, (proxy, method, args) -> { + if (method.getName().equals("setNull")) { + assertThat(args[1]).isEqualTo(nullJdbcType); + return null; + } + throw new UnsupportedOperationException("Unexpected method call: " + method.getName()); + }); + + writeFunction.setNull(statement, 1); + } + + private void testTimestampWriteMapping(Type type, String bindExpression, int nullJdbcType) + throws SQLException + { + WriteMapping writeMapping = CLIENT.toWriteMapping(SESSION, type); + assertThat(writeMapping.getWriteFunction()).isNotNull(); + WriteFunction writeFunction = writeMapping.getWriteFunction(); + + assertThat(writeFunction.getBindExpression()).isEqualTo(bindExpression); + + PreparedStatement statement = newProxy(PreparedStatement.class, (proxy, method, args) -> { + if (method.getName().equals("setNull")) { + assertThat(args[1]).isEqualTo(nullJdbcType); + return null; + } + throw new UnsupportedOperationException("Unexpected method call: " + method.getName()); + }); + + writeFunction.setNull(statement, 1); + } +} diff --git a/plugin/trino-tibero/src/test/java/io/trino/plugin/tibero/TestTiberoPlugin.java b/plugin/trino-tibero/src/test/java/io/trino/plugin/tibero/TestTiberoPlugin.java new file mode 100644 index 000000000000..bce243708cb4 --- /dev/null +++ b/plugin/trino-tibero/src/test/java/io/trino/plugin/tibero/TestTiberoPlugin.java @@ -0,0 +1,38 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.plugin.tibero; + +import com.google.common.collect.ImmutableMap; +import io.trino.spi.Plugin; +import io.trino.spi.connector.ConnectorFactory; +import io.trino.testing.TestingConnectorContext; +import org.junit.jupiter.api.Test; + +import static com.google.common.collect.Iterables.getOnlyElement; + +public class TestTiberoPlugin +{ + @Test + public void testCreateConnector() + { + Plugin plugin = new TiberoPlugin(); + ConnectorFactory factory = getOnlyElement(plugin.getConnectorFactories()); + factory.create( + "test", + ImmutableMap.of( + "connection-url", "jdbc:tibero:thin:@//localhost:8629/tibero", + "bootstrap.quiet", "true"), + new TestingConnectorContext()).shutdown(); + } +} diff --git a/plugin/trino-tibero/src/test/java/io/trino/plugin/tibero/TestTiberoTypeMapping.java b/plugin/trino-tibero/src/test/java/io/trino/plugin/tibero/TestTiberoTypeMapping.java new file mode 100644 index 000000000000..7c299e82826b --- /dev/null +++ b/plugin/trino-tibero/src/test/java/io/trino/plugin/tibero/TestTiberoTypeMapping.java @@ -0,0 +1,285 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.plugin.tibero; + +import io.trino.plugin.base.mapping.DefaultIdentifierMapping; +import io.trino.plugin.jdbc.BaseJdbcConfig; +import io.trino.plugin.jdbc.ColumnMapping; +import io.trino.plugin.jdbc.DefaultQueryBuilder; +import io.trino.plugin.jdbc.JdbcTypeHandle; +import io.trino.plugin.jdbc.logging.RemoteQueryModifier; +import io.trino.spi.connector.ConnectorSession; +import io.trino.spi.type.BigintType; +import io.trino.spi.type.CharType; +import io.trino.spi.type.DecimalType; +import io.trino.spi.type.DoubleType; +import io.trino.spi.type.IntegerType; +import io.trino.spi.type.RealType; +import io.trino.spi.type.SmallintType; +import io.trino.spi.type.TimestampType; +import io.trino.spi.type.VarbinaryType; +import io.trino.spi.type.VarcharType; +import io.trino.testing.TestingConnectorSession; +import org.junit.jupiter.api.Test; + +import java.sql.Connection; +import java.sql.Types; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Type mapping tests for Tibero connector. + * This class tests the mapping between Tibero database types and Trino types. + */ +public class TestTiberoTypeMapping +{ + private static final TiberoClient CLIENT = new TiberoClient( + new BaseJdbcConfig(), + session -> { + throw new UnsupportedOperationException(); + }, + new DefaultQueryBuilder(RemoteQueryModifier.NONE), + new DefaultIdentifierMapping(), + RemoteQueryModifier.NONE); + + private static final ConnectorSession SESSION = TestingConnectorSession.SESSION; + private static final Connection CONNECTION = null; // Not needed for these tests + + @Test + public void testSmallintMapping() + { + assertThat(toTrinoType(Types.SMALLINT, "smallint")) + .hasValueSatisfying(mapping -> assertThat(mapping.getType()).isEqualTo(SmallintType.SMALLINT)); + } + + @Test + public void testIntegerMapping() + { + assertThat(toTrinoType(Types.INTEGER, "integer")) + .hasValueSatisfying(mapping -> assertThat(mapping.getType()).isEqualTo(IntegerType.INTEGER)); + } + + @Test + public void testBigintMapping() + { + assertThat(toTrinoType(Types.BIGINT, "bigint")) + .hasValueSatisfying(mapping -> assertThat(mapping.getType()).isEqualTo(BigintType.BIGINT)); + } + + @Test + public void testRealMapping() + { + assertThat(toTrinoType(Types.REAL, "real")) + .hasValueSatisfying(mapping -> assertThat(mapping.getType()).isEqualTo(RealType.REAL)); + } + + @Test + public void testDoubleMapping() + { + assertThat(toTrinoType(Types.DOUBLE, "double precision")) + .hasValueSatisfying(mapping -> assertThat(mapping.getType()).isEqualTo(DoubleType.DOUBLE)); + } + + @Test + public void testCharMapping() + { + // CHAR(10) + assertThat(toTrinoType(Types.CHAR, "char", 10, 0)) + .hasValueSatisfying(mapping -> { + assertThat(mapping.getType()).isEqualTo(CharType.createCharType(10)); + }); + + // CHAR(255) + assertThat(toTrinoType(Types.CHAR, "char", 255, 0)) + .hasValueSatisfying(mapping -> { + assertThat(mapping.getType()).isEqualTo(CharType.createCharType(255)); + }); + } + + @Test + public void testVarcharMapping() + { + // VARCHAR(10) + assertThat(toTrinoType(Types.VARCHAR, "varchar", 10, 0)) + .hasValueSatisfying(mapping -> { + assertThat(mapping.getType()).isEqualTo(VarcharType.createVarcharType(10)); + }); + + // VARCHAR(255) + assertThat(toTrinoType(Types.VARCHAR, "varchar", 255, 0)) + .hasValueSatisfying(mapping -> { + assertThat(mapping.getType()).isEqualTo(VarcharType.createVarcharType(255)); + }); + + // VARCHAR(65535) + assertThat(toTrinoType(Types.VARCHAR, "varchar", 65535, 0)) + .hasValueSatisfying(mapping -> { + assertThat(mapping.getType()).isEqualTo(VarcharType.createVarcharType(65535)); + }); + } + + @Test + public void testNvarcharMapping() + { + // NVARCHAR(10) + assertThat(toTrinoType(Types.NVARCHAR, "nvarchar", 10, 0)) + .hasValueSatisfying(mapping -> { + assertThat(mapping.getType()).isEqualTo(VarcharType.createVarcharType(10)); + }); + } + + @Test + public void testDecimalMapping() + { + // DECIMAL(3, 0) + assertThat(toTrinoType(Types.DECIMAL, "decimal", 3, 0)) + .hasValueSatisfying(mapping -> { + assertThat(mapping.getType()).isEqualTo(DecimalType.createDecimalType(3, 0)); + }); + + // DECIMAL(10, 2) + assertThat(toTrinoType(Types.DECIMAL, "decimal", 10, 2)) + .hasValueSatisfying(mapping -> { + assertThat(mapping.getType()).isEqualTo(DecimalType.createDecimalType(10, 2)); + }); + + // DECIMAL(38, 0) + assertThat(toTrinoType(Types.DECIMAL, "decimal", 38, 0)) + .hasValueSatisfying(mapping -> { + assertThat(mapping.getType()).isEqualTo(DecimalType.createDecimalType(38, 0)); + }); + + // DECIMAL(38, 38) + assertThat(toTrinoType(Types.DECIMAL, "decimal", 38, 38)) + .hasValueSatisfying(mapping -> { + assertThat(mapping.getType()).isEqualTo(DecimalType.createDecimalType(38, 38)); + }); + } + + @Test + public void testNumericMapping() + { + // NUMERIC is same as DECIMAL in Tibero + assertThat(toTrinoType(Types.NUMERIC, "numeric", 10, 2)) + .hasValueSatisfying(mapping -> { + assertThat(mapping.getType()).isEqualTo(DecimalType.createDecimalType(10, 2)); + }); + } + + @Test + public void testDateMapping() + { + // Tibero DATE maps to TIMESTAMP(0) in Trino + assertThat(toTrinoType("date")) + .hasValueSatisfying(mapping -> { + assertThat(mapping.getType()).isEqualTo(TimestampType.TIMESTAMP_SECONDS); + }); + } + + @Test + public void testTimestampMapping() + { + // TIMESTAMP(0) + assertThat(toTrinoType(Types.TIMESTAMP, "timestamp", 0, 0)) + .hasValueSatisfying(mapping -> { + assertThat(mapping.getType()).isEqualTo(TimestampType.createTimestampType(0)); + }); + + // TIMESTAMP(3) + assertThat(toTrinoType(Types.TIMESTAMP, "timestamp", 0, 3)) + .hasValueSatisfying(mapping -> { + assertThat(mapping.getType()).isEqualTo(TimestampType.createTimestampType(3)); + }); + + // TIMESTAMP(6) + assertThat(toTrinoType(Types.TIMESTAMP, "timestamp", 0, 6)) + .hasValueSatisfying(mapping -> { + assertThat(mapping.getType()).isEqualTo(TimestampType.createTimestampType(6)); + }); + + // TIMESTAMP(9) + assertThat(toTrinoType(Types.TIMESTAMP, "timestamp", 0, 9)) + .hasValueSatisfying(mapping -> { + assertThat(mapping.getType()).isEqualTo(TimestampType.createTimestampType(9)); + }); + } + + @Test + public void testVarbinaryMapping() + { + // Tibero RAW(n) maps to VARBINARY + assertThat(toTrinoType(Types.VARBINARY, "raw", 100, 0)) + .hasValueSatisfying(mapping -> { + assertThat(mapping.getType()).isEqualTo(VarbinaryType.VARBINARY); + }); + } + + @Test + public void testBlobMapping() + { + // BLOB maps to VARBINARY + assertThat(toTrinoType(Types.BLOB, "blob")) + .hasValueSatisfying(mapping -> { + assertThat(mapping.getType()).isEqualTo(VarbinaryType.VARBINARY); + }); + } + + @Test + public void testClobMapping() + { + // CLOB maps to unbounded VARCHAR + assertThat(toTrinoType(Types.CLOB, "clob")) + .hasValueSatisfying(mapping -> { + assertThat(mapping.getType()).isEqualTo(VarcharType.createUnboundedVarcharType()); + }); + } + + @Test + public void testNclobMapping() + { + // NCLOB maps to unbounded VARCHAR + assertThat(toTrinoType(Types.NCLOB, "nclob")) + .hasValueSatisfying(mapping -> { + assertThat(mapping.getType()).isEqualTo(VarcharType.createUnboundedVarcharType()); + }); + } + + private Optional toTrinoType(int jdbcType, String jdbcTypeName) + { + return toTrinoType(jdbcType, jdbcTypeName, 0, 0); + } + + private Optional toTrinoType(String jdbcTypeName) + { + return CLIENT.toColumnMapping(SESSION, CONNECTION, new JdbcTypeHandle( + Types.OTHER, + Optional.of(jdbcTypeName), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty())); + } + + private Optional toTrinoType(int jdbcType, String jdbcTypeName, int columnSize, int decimalDigits) + { + return CLIENT.toColumnMapping(SESSION, CONNECTION, new JdbcTypeHandle( + jdbcType, + Optional.of(jdbcTypeName), + Optional.of(columnSize), + Optional.of(decimalDigits), + Optional.empty(), + Optional.empty())); + } +} diff --git a/pom.xml b/pom.xml index 451ecbf5f0cf..494b6bd684a8 100644 --- a/pom.xml +++ b/pom.xml @@ -118,6 +118,7 @@ plugin/trino-thrift plugin/trino-thrift-api plugin/trino-thrift-testing-server + plugin/trino-tibero plugin/trino-tpcds plugin/trino-tpch service/trino-proxy