diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 63b38e65cea2..96f1bfb5bf95 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -324,6 +324,7 @@ jobs: !:trino-mysql, !:trino-postgresql, !:trino-sqlserver, + !:trino-mariadb, !:trino-oracle, !:trino-kudu, !:trino-druid, @@ -402,6 +403,7 @@ jobs: - { modules: plugin/trino-postgresql } - { modules: plugin/trino-sqlserver } - { modules: plugin/trino-singlestore } + - { modules: plugin/trino-mariadb } - { modules: plugin/trino-oracle } - { modules: plugin/trino-kudu } - { modules: plugin/trino-druid } diff --git a/core/trino-server/src/main/provisio/trino.xml b/core/trino-server/src/main/provisio/trino.xml index 9bfbbfb39f4d..5860fec127da 100644 --- a/core/trino-server/src/main/provisio/trino.xml +++ b/core/trino-server/src/main/provisio/trino.xml @@ -128,6 +128,12 @@ + + + + + + diff --git a/docs/src/main/sphinx/connector.rst b/docs/src/main/sphinx/connector.rst index 5ece65e24f7b..54cca76b3874 100644 --- a/docs/src/main/sphinx/connector.rst +++ b/docs/src/main/sphinx/connector.rst @@ -25,6 +25,7 @@ from different data sources. Kinesis Kudu Local File + MariaDB Memory MongoDB MySQL diff --git a/docs/src/main/sphinx/connector/mariadb.rst b/docs/src/main/sphinx/connector/mariadb.rst new file mode 100644 index 000000000000..b06cea9320c0 --- /dev/null +++ b/docs/src/main/sphinx/connector/mariadb.rst @@ -0,0 +1,162 @@ +================= +MariaDB connector +================= + +The MariaDB connector allows querying and +creating tables in an external MariaDB database. + +Requirements +------------ + +To connect to MariaDB, you need: + +* MariaDB version 10.2 or higher. +* Network access from the Trino coordinator and workers to MariaDB. Port + 3306 is the default port. + +Configuration +------------- + +To configure the MariaDB connector, create a catalog properties file +in ``etc/catalog`` named, for example, ``mariadb.properties``, to +mount the MariaDB connector as the ``mariadb`` catalog. +Create the file with the following contents, replacing the +connection properties as appropriate for your setup: + +.. code-block:: text + + connector.name=mariadb + connection-url=jdbc:mariadb://example.net:3306 + connection-user=root + connection-password=secret + +.. include:: jdbc-common-configurations.fragment + +.. include:: jdbc-case-insensitive-matching.fragment + +.. include:: non-transactional-insert.fragment + +Querying MariaDB +---------------- + +The MariaDB connector provides a schema for every MariaDB *database*. +You can see the available MariaDB databases by running ``SHOW SCHEMAS``:: + + SHOW SCHEMAS FROM mariadb; + +If you have a MariaDB database named ``web``, you can view the tables +in this database by running ``SHOW TABLES``:: + + SHOW TABLES FROM mariadb.web; + +You can see a list of the columns in the ``clicks`` table in the ``web`` +database using either of the following:: + + DESCRIBE mariadb.web.clicks; + SHOW COLUMNS FROM mariadb.web.clicks; + +Finally, you can access the ``clicks`` table in the ``web`` database:: + + SELECT * FROM mariadb.web.clicks; + +If you used a different name for your catalog properties file, use +that catalog name instead of ``mariadb`` in the above examples. + +.. mariadb-type-mapping: + +Type mapping +------------ + +Trino supports the following MariaDB data types: + +================================== =============================== +MariaDB Type Trino Type +================================== =============================== +``boolean`` ``tinyint`` +``tinyint`` ``tinyint`` +``smallint`` ``smallint`` +``int`` ``integer`` +``bigint`` ``bigint`` +``tinyint unsigned`` ``smallint`` +``smallint unsigned`` ``integer`` +``mediumint unsigned`` ``integer`` +``integer unsigned`` ``bigint`` +``bigint unsigned`` ``decimal(20,0)`` +``float`` ``real`` +``double`` ``double`` +``decimal(p,s)`` ``decimal(p,s)`` +``char(n)`` ``char(n)`` +``tinytext`` ``varchar(255)`` +``text`` ``varchar(65535)`` +``mediumtext`` ``varchar(16777215)`` +``longtext`` ``varchar`` +``varchar(n)`` ``varchar(n)`` +``tinyblob`` ``varbinary`` +``blob`` ``varbinary`` +``mediumblob`` ``varbinary`` +``longblob`` ``varbinary`` +``varbinary(n)`` ``varbinary`` +``date`` ``date`` +``time(n)`` ``time(n)`` +================================== =============================== + +Complete list of `MariaDB data types +`_. + + +.. include:: jdbc-type-mapping.fragment + +.. _mariadb-sql-support: + +SQL support +----------- + +The connector provides read access and write access to data and metadata in +a MariaDB database. In addition to the :ref:`globally available +` and :ref:`read operation ` +statements, the connector supports the following features: + +* :doc:`/sql/insert` +* :doc:`/sql/delete` +* :doc:`/sql/truncate` +* :doc:`/sql/create-table` +* :doc:`/sql/create-table-as` +* :doc:`/sql/drop-table` +* :doc:`/sql/alter-table` +* :doc:`/sql/create-schema` +* :doc:`/sql/drop-schema` + +.. include:: sql-delete-limitation.fragment + +Performance +----------- + +The connector includes a number of performance improvements, detailed in the +following sections. + +.. _mariadb-pushdown: + +Pushdown +^^^^^^^^ + +The connector supports pushdown for a number of operations: + +* :ref:`join-pushdown` +* :ref:`limit-pushdown` +* :ref:`topn-pushdown` + +:ref:`Aggregate pushdown ` for the following functions: + +* :func:`avg` +* :func:`count` +* :func:`max` +* :func:`min` +* :func:`sum` +* :func:`stddev` +* :func:`stddev_pop` +* :func:`stddev_samp` +* :func:`variance` +* :func:`var_pop` +* :func:`var_samp` + +.. include:: no-pushdown-text-type.fragment diff --git a/plugin/trino-mariadb/pom.xml b/plugin/trino-mariadb/pom.xml new file mode 100644 index 000000000000..48c5a58b28c7 --- /dev/null +++ b/plugin/trino-mariadb/pom.xml @@ -0,0 +1,164 @@ + + + 4.0.0 + + + io.trino + trino-root + 379-SNAPSHOT + ../../pom.xml + + + trino-mariadb + Trino - MariaDB Connector + trino-plugin + + + ${project.parent.basedir} + + + + + io.trino + trino-base-jdbc + + + + io.trino + trino-plugin-toolkit + + + + com.google.guava + guava + + + + com.google.inject + guice + + + + javax.inject + javax.inject + + + + org.mariadb.jdbc + mariadb-java-client + + + + + io.airlift + log + runtime + + + + io.airlift + log-manager + runtime + + + + + io.trino + trino-spi + provided + + + + io.airlift + slice + provided + + + + com.fasterxml.jackson.core + jackson-annotations + provided + + + + org.openjdk.jol + jol-core + provided + + + + + io.trino + trino-base-jdbc + test-jar + test + + + + io.trino + trino-main + test + + + + io.trino + trino-main + test-jar + test + + + + io.trino + trino-testing + test + + + + io.trino + trino-tpch + test + + + + io.trino.tpch + tpch + test + + + + io.airlift + testing + test + + + + org.assertj + assertj-core + test + + + + org.jetbrains + annotations + test + + + + org.testcontainers + mariadb + test + + + + org.testcontainers + testcontainers + test + + + + org.testng + testng + test + + + diff --git a/plugin/trino-mariadb/src/main/java/io/trino/plugin/mariadb/ImplementAvgBigint.java b/plugin/trino-mariadb/src/main/java/io/trino/plugin/mariadb/ImplementAvgBigint.java new file mode 100644 index 000000000000..4a0c3d16a04f --- /dev/null +++ b/plugin/trino-mariadb/src/main/java/io/trino/plugin/mariadb/ImplementAvgBigint.java @@ -0,0 +1,26 @@ +/* + * 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.mariadb; + +import io.trino.plugin.jdbc.aggregation.BaseImplementAvgBigint; + +public class ImplementAvgBigint + extends BaseImplementAvgBigint +{ + @Override + protected String getRewriteFormatExpression() + { + return "avg((%s * 1.0))"; + } +} diff --git a/plugin/trino-mariadb/src/main/java/io/trino/plugin/mariadb/MariaDbClient.java b/plugin/trino-mariadb/src/main/java/io/trino/plugin/mariadb/MariaDbClient.java new file mode 100644 index 000000000000..ee950ce1135e --- /dev/null +++ b/plugin/trino-mariadb/src/main/java/io/trino/plugin/mariadb/MariaDbClient.java @@ -0,0 +1,567 @@ +/* + * 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.mariadb; + +import com.google.common.collect.ImmutableSet; +import io.trino.plugin.base.aggregation.AggregateFunctionRewriter; +import io.trino.plugin.base.aggregation.AggregateFunctionRule; +import io.trino.plugin.base.expression.ConnectorExpressionRewriter; +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.JdbcColumnHandle; +import io.trino.plugin.jdbc.JdbcExpression; +import io.trino.plugin.jdbc.JdbcJoinCondition; +import io.trino.plugin.jdbc.JdbcSortItem; +import io.trino.plugin.jdbc.JdbcTableHandle; +import io.trino.plugin.jdbc.JdbcTypeHandle; +import io.trino.plugin.jdbc.LongWriteFunction; +import io.trino.plugin.jdbc.PreparedQuery; +import io.trino.plugin.jdbc.QueryBuilder; +import io.trino.plugin.jdbc.WriteMapping; +import io.trino.plugin.jdbc.aggregation.ImplementAvgDecimal; +import io.trino.plugin.jdbc.aggregation.ImplementAvgFloatingPoint; +import io.trino.plugin.jdbc.aggregation.ImplementCount; +import io.trino.plugin.jdbc.aggregation.ImplementCountAll; +import io.trino.plugin.jdbc.aggregation.ImplementMinMax; +import io.trino.plugin.jdbc.aggregation.ImplementStddevPop; +import io.trino.plugin.jdbc.aggregation.ImplementStddevSamp; +import io.trino.plugin.jdbc.aggregation.ImplementSum; +import io.trino.plugin.jdbc.aggregation.ImplementVariancePop; +import io.trino.plugin.jdbc.aggregation.ImplementVarianceSamp; +import io.trino.plugin.jdbc.expression.JdbcConnectorExpressionRewriterBuilder; +import io.trino.plugin.jdbc.mapping.IdentifierMapping; +import io.trino.spi.TrinoException; +import io.trino.spi.connector.AggregateFunction; +import io.trino.spi.connector.ColumnHandle; +import io.trino.spi.connector.ConnectorSession; +import io.trino.spi.connector.JoinCondition; +import io.trino.spi.connector.JoinStatistics; +import io.trino.spi.connector.JoinType; +import io.trino.spi.connector.SchemaTableName; +import io.trino.spi.type.CharType; +import io.trino.spi.type.DecimalType; +import io.trino.spi.type.Decimals; +import io.trino.spi.type.TimeType; +import io.trino.spi.type.Type; +import io.trino.spi.type.VarcharType; + +import javax.inject.Inject; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.SQLSyntaxErrorException; +import java.sql.Types; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.BiFunction; +import java.util.stream.Stream; + +import static com.google.common.base.Verify.verify; +import static io.trino.plugin.jdbc.DecimalConfig.DecimalMapping.ALLOW_OVERFLOW; +import static io.trino.plugin.jdbc.DecimalSessionSessionProperties.getDecimalDefaultScale; +import static io.trino.plugin.jdbc.DecimalSessionSessionProperties.getDecimalRounding; +import static io.trino.plugin.jdbc.DecimalSessionSessionProperties.getDecimalRoundingMode; +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.booleanWriteFunction; +import static io.trino.plugin.jdbc.StandardColumnMappings.charWriteFunction; +import static io.trino.plugin.jdbc.StandardColumnMappings.decimalColumnMapping; +import static io.trino.plugin.jdbc.StandardColumnMappings.defaultCharColumnMapping; +import static io.trino.plugin.jdbc.StandardColumnMappings.defaultVarcharColumnMapping; +import static io.trino.plugin.jdbc.StandardColumnMappings.doubleColumnMapping; +import static io.trino.plugin.jdbc.StandardColumnMappings.doubleWriteFunction; +import static io.trino.plugin.jdbc.StandardColumnMappings.integerColumnMapping; +import static io.trino.plugin.jdbc.StandardColumnMappings.integerWriteFunction; +import static io.trino.plugin.jdbc.StandardColumnMappings.longDecimalWriteFunction; +import static io.trino.plugin.jdbc.StandardColumnMappings.realWriteFunction; +import static io.trino.plugin.jdbc.StandardColumnMappings.shortDecimalWriteFunction; +import static io.trino.plugin.jdbc.StandardColumnMappings.smallintColumnMapping; +import static io.trino.plugin.jdbc.StandardColumnMappings.smallintWriteFunction; +import static io.trino.plugin.jdbc.StandardColumnMappings.timeColumnMapping; +import static io.trino.plugin.jdbc.StandardColumnMappings.timeWriteFunction; +import static io.trino.plugin.jdbc.StandardColumnMappings.tinyintColumnMapping; +import static io.trino.plugin.jdbc.StandardColumnMappings.tinyintWriteFunction; +import static io.trino.plugin.jdbc.StandardColumnMappings.varbinaryReadFunction; +import static io.trino.plugin.jdbc.StandardColumnMappings.varbinaryWriteFunction; +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.BooleanType.BOOLEAN; +import static io.trino.spi.type.DateType.DATE; +import static io.trino.spi.type.DecimalType.createDecimalType; +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.TimeType.createTimeType; +import static io.trino.spi.type.TinyintType.TINYINT; +import static io.trino.spi.type.VarbinaryType.VARBINARY; +import static java.lang.Float.floatToRawIntBits; +import static java.lang.Math.max; +import static java.lang.Math.min; +import static java.lang.String.format; +import static java.util.stream.Collectors.joining; + +public class MariaDbClient + extends BaseJdbcClient +{ + private static final int MAX_SUPPORTED_DATE_TIME_PRECISION = 6; + // MariaDB driver returns width of time types instead of precision. + private static final int ZERO_PRECISION_TIME_COLUMN_SIZE = 10; + + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("uuuu-MM-dd"); + + private final AggregateFunctionRewriter aggregateFunctionRewriter; + + @Inject + public MariaDbClient(BaseJdbcConfig config, ConnectionFactory connectionFactory, QueryBuilder queryBuilder, IdentifierMapping identifierMapping) + { + super(config, "`", connectionFactory, queryBuilder, identifierMapping); + + JdbcTypeHandle bigintTypeHandle = new JdbcTypeHandle(Types.BIGINT, Optional.of("bigint"), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty()); + ConnectorExpressionRewriter connectorExpressionRewriter = JdbcConnectorExpressionRewriterBuilder.newBuilder() + .addStandardRules(this::quoted) + .build(); + this.aggregateFunctionRewriter = new AggregateFunctionRewriter<>( + connectorExpressionRewriter, + ImmutableSet.>builder() + .add(new ImplementCountAll(bigintTypeHandle)) + .add(new ImplementCount(bigintTypeHandle)) + .add(new ImplementMinMax(false)) + .add(new ImplementSum(MariaDbClient::toTypeHandle)) + .add(new ImplementAvgFloatingPoint()) + .add(new ImplementAvgDecimal()) + .add(new ImplementAvgBigint()) + .add(new ImplementStddevSamp()) + .add(new ImplementStddevPop()) + .add(new ImplementVarianceSamp()) + .add(new ImplementVariancePop()) + .build()); + } + + @Override + public Optional implementAggregation(ConnectorSession session, AggregateFunction aggregate, Map assignments) + { + // TODO support complex ConnectorExpressions + return aggregateFunctionRewriter.rewrite(session, aggregate, assignments); + } + + @Override + public boolean supportsAggregationPushdown(ConnectorSession session, JdbcTableHandle table, List aggregates, Map assignments, List> groupingSets) + { + // Remote database can be case insensitive. + return preventTextualTypeAggregationPushdown(groupingSets); + } + + private static Optional toTypeHandle(DecimalType decimalType) + { + return Optional.of(new JdbcTypeHandle(Types.NUMERIC, Optional.of("decimal"), Optional.of(decimalType.getPrecision()), Optional.of(decimalType.getScale()), Optional.empty(), Optional.empty())); + } + + @Override + public Collection listSchemas(Connection connection) + { + // for MariaDB, we need to list catalogs instead of schemas + try (ResultSet resultSet = connection.getMetaData().getCatalogs()) { + ImmutableSet.Builder schemaNames = ImmutableSet.builder(); + while (resultSet.next()) { + String schemaName = resultSet.getString("TABLE_CAT"); + if (filterSchema(schemaName)) { + schemaNames.add(schemaName); + } + } + return schemaNames.build(); + } + catch (SQLException e) { + throw new TrinoException(JDBC_ERROR, e); + } + } + + @Override + protected boolean filterSchema(String schemaName) + { + // MariaDB has 'mysql' schema + if (schemaName.equalsIgnoreCase("mysql") + || schemaName.equalsIgnoreCase("performance_schema")) { + return false; + } + return super.filterSchema(schemaName); + } + + @Override + public void renameSchema(ConnectorSession session, String schemaName, String newSchemaName) + { + throw new TrinoException(NOT_SUPPORTED, "This connector does not support renaming schemas"); + } + + @Override + public ResultSet getTables(Connection connection, Optional schemaName, Optional tableName) + throws SQLException + { + // MariaDB maps their "database" to SQL catalogs and does not have schemas + DatabaseMetaData metadata = connection.getMetaData(); + return metadata.getTables( + schemaName.orElse(null), + null, + escapeNamePattern(tableName, metadata.getSearchStringEscape()).orElse(null), + getTableTypes().map(types -> types.toArray(String[]::new)).orElse(null)); + } + + @Override + protected String getTableSchemaName(ResultSet resultSet) + throws SQLException + { + // MariaDB uses catalogs instead of schemas + return resultSet.getString("TABLE_CAT"); + } + + @Override + public Optional getTableComment(ResultSet resultSet) + { + // Don't return a comment until the connector supports creating tables with comment + return Optional.empty(); + } + + @Override + public Optional toColumnMapping(ConnectorSession session, Connection connection, JdbcTypeHandle typeHandle) + { + Optional mapping = getForcedMappingToVarchar(typeHandle); + if (mapping.isPresent()) { + return mapping; + } + Optional unsignedMapping = getUnsignedMapping(typeHandle); + if (unsignedMapping.isPresent()) { + return unsignedMapping; + } + + switch (typeHandle.getJdbcType()) { + case Types.TINYINT: + return Optional.of(tinyintColumnMapping()); + 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(ColumnMapping.longMapping( + REAL, + (resultSet, columnIndex) -> floatToRawIntBits(resultSet.getFloat(columnIndex)), + realWriteFunction(), + DISABLE_PUSHDOWN)); + case Types.DOUBLE: + return Optional.of(doubleColumnMapping()); + case Types.NUMERIC: + case Types.DECIMAL: + int decimalDigits = typeHandle.getRequiredDecimalDigits(); + int precision = typeHandle.getRequiredColumnSize(); + if (getDecimalRounding(session) == ALLOW_OVERFLOW && precision > Decimals.MAX_PRECISION) { + int scale = min(decimalDigits, getDecimalDefaultScale(session)); + return Optional.of(decimalColumnMapping(createDecimalType(Decimals.MAX_PRECISION, scale), getDecimalRoundingMode(session))); + } + precision = precision + max(-decimalDigits, 0); // Map decimal(p, -s) (negative scale) to decimal(p+s, 0). + if (precision > Decimals.MAX_PRECISION) { + break; + } + return Optional.of(decimalColumnMapping(createDecimalType(precision, max(decimalDigits, 0)))); + case Types.CHAR: + return Optional.of(defaultCharColumnMapping(typeHandle.getRequiredColumnSize(), false)); + case Types.VARCHAR: + case Types.LONGVARCHAR: + return Optional.of(defaultVarcharColumnMapping(typeHandle.getRequiredColumnSize(), false)); + case Types.BINARY: + case Types.VARBINARY: + case Types.LONGVARBINARY: + return Optional.of(ColumnMapping.sliceMapping(VARBINARY, varbinaryReadFunction(), varbinaryWriteFunction(), FULL_PUSHDOWN)); + case Types.DATE: + return Optional.of(ColumnMapping.longMapping( + DATE, + // Use StandardColumnMappings.java.dateReadFunctionUsingLocalDate after merged https://github.com/trinodb/trino/pull/10054 + (resultSet, index) -> resultSet.getObject(index, LocalDate.class).toEpochDay(), + dateWriteFunction())); + case Types.TIME: + TimeType timeType = createTimeType(getTimePrecision(typeHandle.getRequiredColumnSize())); + return Optional.of(timeColumnMapping(timeType)); + } + + if (getUnsupportedTypeHandling(session) == CONVERT_TO_VARCHAR) { + return mapToUnboundedVarchar(typeHandle); + } + return Optional.empty(); + } + + private static int getTimePrecision(int timeColumnSize) + { + if (timeColumnSize == ZERO_PRECISION_TIME_COLUMN_SIZE) { + return 0; + } + int timePrecision = timeColumnSize - ZERO_PRECISION_TIME_COLUMN_SIZE - 1; + verify(1 <= timePrecision && timePrecision <= MAX_SUPPORTED_DATE_TIME_PRECISION, "Unexpected time precision %s calculated from time column size %s", timePrecision, timeColumnSize); + return timePrecision; + } + + @Override + public WriteMapping toWriteMapping(ConnectorSession session, Type type) + { + if (type == BOOLEAN) { + return WriteMapping.booleanMapping("boolean", booleanWriteFunction()); + } + if (type == TINYINT) { + return WriteMapping.longMapping("tinyint", tinyintWriteFunction()); + } + 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("float", realWriteFunction()); + } + if (type == DOUBLE) { + return WriteMapping.doubleMapping("double precision", doubleWriteFunction()); + } + if (type instanceof DecimalType) { + DecimalType decimalType = (DecimalType) type; + String dataType = format("decimal(%s, %s)", decimalType.getPrecision(), decimalType.getScale()); + if (decimalType.isShort()) { + return WriteMapping.longMapping(dataType, shortDecimalWriteFunction(decimalType)); + } + return WriteMapping.objectMapping(dataType, longDecimalWriteFunction(decimalType)); + } + if (type instanceof CharType) { + return WriteMapping.sliceMapping("char(" + ((CharType) type).getLength() + ")", charWriteFunction()); + } + if (type instanceof VarcharType) { + VarcharType varcharType = (VarcharType) type; + String dataType; + if (varcharType.isUnbounded()) { + dataType = "longtext"; + } + else if (varcharType.getBoundedLength() <= 255) { + dataType = "tinytext"; + } + else if (varcharType.getBoundedLength() <= 65535) { + dataType = "text"; + } + else if (varcharType.getBoundedLength() <= 16777215) { + dataType = "mediumtext"; + } + else { + dataType = "longtext"; + } + return WriteMapping.sliceMapping(dataType, varcharWriteFunction()); + } + if (type == VARBINARY) { + return WriteMapping.sliceMapping("mediumblob", varbinaryWriteFunction()); + } + if (type == DATE) { + return WriteMapping.longMapping("date", dateWriteFunction()); + } + if (type instanceof TimeType) { + TimeType timeType = (TimeType) type; + if (timeType.getPrecision() <= MAX_SUPPORTED_DATE_TIME_PRECISION) { + return WriteMapping.longMapping(format("time(%s)", timeType.getPrecision()), timeWriteFunction(timeType.getPrecision())); + } + return WriteMapping.longMapping(format("time(%s)", MAX_SUPPORTED_DATE_TIME_PRECISION), timeWriteFunction(MAX_SUPPORTED_DATE_TIME_PRECISION)); + } + + throw new TrinoException(NOT_SUPPORTED, "Unsupported column type: " + type.getDisplayName()); + } + + @Override + public void renameColumn(ConnectorSession session, JdbcTableHandle handle, JdbcColumnHandle jdbcColumn, String newColumnName) + { + try (Connection connection = connectionFactory.openConnection(session)) { + String newRemoteColumnName = getIdentifierMapping().toRemoteColumnName(connection, newColumnName); + // MariaDB versions earlier than 10.5.2 do not support the RENAME COLUMN syntax + // ALTER TABLE ... CHANGE statement exists in th old versions, but it requires providing all attributes of the column + String sql = format( + "ALTER TABLE %s RENAME COLUMN %s TO %s", + quoted(handle.getCatalogName(), handle.getSchemaName(), handle.getTableName()), + quoted(jdbcColumn.getColumnName()), + quoted(newRemoteColumnName)); + execute(connection, sql); + } + catch (TrinoException e) { + if (e.getCause() instanceof SQLSyntaxErrorException) { + throw new TrinoException(NOT_SUPPORTED, "Rename column not supported for the MariaDB server version", e); + } + throw e; + } + catch (SQLException e) { + throw new TrinoException(JDBC_ERROR, e); + } + } + + @Override + protected void copyTableSchema(Connection connection, String catalogName, String schemaName, String tableName, String newTableName, List columnNames) + { + // Copy all columns for enforcing NOT NULL option in the temp table + String tableCopyFormat = "CREATE TABLE %s AS SELECT * FROM %s WHERE 0 = 1"; + String sql = format( + tableCopyFormat, + quoted(catalogName, schemaName, newTableName), + quoted(catalogName, schemaName, tableName)); + execute(connection, sql); + } + + @Override + public void renameTable(ConnectorSession session, JdbcTableHandle handle, SchemaTableName newTableName) + { + // MariaDB doesn't support specifying the catalog name in a rename. By setting the + // catalogName parameter to null, it will be omitted in the ALTER TABLE statement. + verify(handle.getSchemaName() == null); + renameTable(session, null, handle.getCatalogName(), handle.getTableName(), newTableName); + } + + @Override + protected Optional> limitFunction() + { + return Optional.of((sql, limit) -> sql + " LIMIT " + limit); + } + + @Override + public boolean isLimitGuaranteed(ConnectorSession session) + { + return true; + } + + @Override + public boolean supportsTopN(ConnectorSession session, JdbcTableHandle handle, List sortOrder) + { + for (JdbcSortItem sortItem : sortOrder) { + Type sortItemType = sortItem.getColumn().getColumnType(); + if (sortItemType instanceof CharType || sortItemType instanceof VarcharType) { + // Remote database can be case insensitive. + return false; + } + } + return true; + } + + @Override + protected Optional topNFunction() + { + return Optional.of((query, sortItems, limit) -> { + String orderBy = sortItems.stream() + .flatMap(sortItem -> { + String ordering = sortItem.getSortOrder().isAscending() ? "ASC" : "DESC"; + String columnSorting = format("%s %s", quoted(sortItem.getColumn().getColumnName()), ordering); + + switch (sortItem.getSortOrder()) { + case ASC_NULLS_FIRST: + // In MariaDB ASC implies NULLS FIRST + case DESC_NULLS_LAST: + // In MariaDB DESC implies NULLS LAST + return Stream.of(columnSorting); + + case ASC_NULLS_LAST: + return Stream.of( + format("ISNULL(%s) ASC", quoted(sortItem.getColumn().getColumnName())), + columnSorting); + case DESC_NULLS_FIRST: + return Stream.of( + format("ISNULL(%s) DESC", quoted(sortItem.getColumn().getColumnName())), + columnSorting); + } + throw new UnsupportedOperationException("Unsupported sort order: " + sortItem.getSortOrder()); + }) + .collect(joining(", ")); + return format("%s ORDER BY %s LIMIT %s", query, orderBy, limit); + }); + } + + @Override + public boolean isTopNGuaranteed(ConnectorSession session) + { + return true; + } + + @Override + public Optional implementJoin( + ConnectorSession session, + JoinType joinType, + PreparedQuery leftSource, + PreparedQuery rightSource, + List joinConditions, + Map rightAssignments, + Map leftAssignments, + JoinStatistics statistics) + { + if (joinType == JoinType.FULL_OUTER) { + // Not supported in MariaDB + return Optional.empty(); + } + return super.implementJoin(session, joinType, leftSource, rightSource, joinConditions, rightAssignments, leftAssignments, statistics); + } + + @Override + protected boolean isSupportedJoinCondition(ConnectorSession session, JdbcJoinCondition joinCondition) + { + if (joinCondition.getOperator() == JoinCondition.Operator.IS_DISTINCT_FROM) { + // Not supported in MariaDB + return false; + } + + // Remote database can be case insensitive. + return Stream.of(joinCondition.getLeftColumn(), joinCondition.getRightColumn()) + .map(JdbcColumnHandle::getColumnType) + .noneMatch(type -> type instanceof CharType || type instanceof VarcharType); + } + + private static LongWriteFunction dateWriteFunction() + { + return (statement, index, day) -> statement.setString(index, DATE_FORMATTER.format(LocalDate.ofEpochDay(day))); + } + + private static Optional getUnsignedMapping(JdbcTypeHandle typeHandle) + { + if (typeHandle.getJdbcTypeName().isEmpty()) { + return Optional.empty(); + } + + String typeName = typeHandle.getJdbcTypeName().get(); + if (typeName.equalsIgnoreCase("tinyint unsigned")) { + return Optional.of(smallintColumnMapping()); + } + if (typeName.equalsIgnoreCase("smallint unsigned")) { + return Optional.of(integerColumnMapping()); + } + if (typeName.equalsIgnoreCase("int unsigned")) { + return Optional.of(bigintColumnMapping()); + } + if (typeName.equalsIgnoreCase("bigint unsigned")) { + return Optional.of(decimalColumnMapping(createDecimalType(20))); + } + + return Optional.empty(); + } +} diff --git a/plugin/trino-mariadb/src/main/java/io/trino/plugin/mariadb/MariaDbClientModule.java b/plugin/trino-mariadb/src/main/java/io/trino/plugin/mariadb/MariaDbClientModule.java new file mode 100644 index 000000000000..e89d84cb2f69 --- /dev/null +++ b/plugin/trino-mariadb/src/main/java/io/trino/plugin/mariadb/MariaDbClientModule.java @@ -0,0 +1,56 @@ +/* + * 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.mariadb; + +import com.google.inject.Binder; +import com.google.inject.Module; +import com.google.inject.Provides; +import com.google.inject.Scopes; +import com.google.inject.Singleton; +import io.trino.plugin.jdbc.BaseJdbcConfig; +import io.trino.plugin.jdbc.ConnectionFactory; +import io.trino.plugin.jdbc.DecimalModule; +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 org.mariadb.jdbc.Driver; + +import java.util.Properties; + +public class MariaDbClientModule + implements Module +{ + @Override + public void configure(Binder binder) + { + binder.bind(JdbcClient.class).annotatedWith(ForBaseJdbc.class).to(MariaDbClient.class).in(Scopes.SINGLETON); + binder.install(new DecimalModule()); + } + + @Provides + @Singleton + @ForBaseJdbc + public static ConnectionFactory createConnectionFactory(BaseJdbcConfig config, CredentialProvider credentialProvider) + { + return new DriverConnectionFactory(new Driver(), config.getConnectionUrl(), getConnectionProperties(), credentialProvider); + } + + private static Properties getConnectionProperties() + { + Properties connectionProperties = new Properties(); + connectionProperties.setProperty("tinyInt1isBit", "false"); + return connectionProperties; + } +} diff --git a/plugin/trino-mariadb/src/main/java/io/trino/plugin/mariadb/MariaDbPlugin.java b/plugin/trino-mariadb/src/main/java/io/trino/plugin/mariadb/MariaDbPlugin.java new file mode 100644 index 000000000000..46274eafe7ff --- /dev/null +++ b/plugin/trino-mariadb/src/main/java/io/trino/plugin/mariadb/MariaDbPlugin.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.mariadb; + +import io.trino.plugin.jdbc.JdbcPlugin; + +public class MariaDbPlugin + extends JdbcPlugin +{ + public MariaDbPlugin() + { + super("mariadb", new MariaDbClientModule()); + } +} diff --git a/plugin/trino-mariadb/src/test/java/io/trino/plugin/mariadb/BaseMariaDbConnectorTest.java b/plugin/trino-mariadb/src/test/java/io/trino/plugin/mariadb/BaseMariaDbConnectorTest.java new file mode 100644 index 000000000000..199842ce10c5 --- /dev/null +++ b/plugin/trino-mariadb/src/test/java/io/trino/plugin/mariadb/BaseMariaDbConnectorTest.java @@ -0,0 +1,284 @@ +/* + * 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.mariadb; + +import io.trino.plugin.jdbc.BaseJdbcConnectorTest; +import io.trino.sql.planner.plan.FilterNode; +import io.trino.testing.MaterializedResult; +import io.trino.testing.TestingConnectorBehavior; +import io.trino.testing.sql.TestTable; +import org.testng.annotations.Test; + +import java.util.Optional; + +import static com.google.common.base.Strings.nullToEmpty; +import static io.trino.spi.type.VarcharType.VARCHAR; +import static io.trino.testing.MaterializedResult.resultBuilder; +import static io.trino.testing.assertions.Assert.assertEquals; +import static java.lang.String.format; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +public abstract class BaseMariaDbConnectorTest + extends BaseJdbcConnectorTest +{ + protected TestingMariaDbServer server; + + @Override + protected boolean hasBehavior(TestingConnectorBehavior connectorBehavior) + { + switch (connectorBehavior) { + case SUPPORTS_JOIN_PUSHDOWN: + case SUPPORTS_AGGREGATION_PUSHDOWN_STDDEV: + case SUPPORTS_AGGREGATION_PUSHDOWN_VARIANCE: + return true; + case SUPPORTS_ADD_COLUMN_WITH_COMMENT: + case SUPPORTS_JOIN_PUSHDOWN_WITH_FULL_JOIN: + case SUPPORTS_JOIN_PUSHDOWN_WITH_DISTINCT_FROM: + case SUPPORTS_PREDICATE_PUSHDOWN_WITH_VARCHAR_EQUALITY: + case SUPPORTS_PREDICATE_PUSHDOWN_WITH_VARCHAR_INEQUALITY: + case SUPPORTS_RENAME_SCHEMA: + case SUPPORTS_COMMENT_ON_TABLE: + case SUPPORTS_COMMENT_ON_COLUMN: + case SUPPORTS_ARRAY: + case SUPPORTS_ROW_TYPE: + case SUPPORTS_NEGATIVE_DATE: + return false; + + default: + return super.hasBehavior(connectorBehavior); + } + } + + @Override + protected TestTable createTableWithDefaultColumns() + { + return new TestTable( + onRemoteDatabase(), + "tpch.table", + "(col_required BIGINT NOT NULL," + + "col_nullable BIGINT," + + "col_default BIGINT DEFAULT 43," + + "col_nonnull_default BIGINT NOT NULL DEFAULT 42," + + "col_required2 BIGINT NOT NULL)"); + } + + @Override + protected TestTable createTableWithUnsupportedColumn() + { + return new TestTable( + onRemoteDatabase(), + "tpch.test_unsupported_column_present", + "(one bigint, two decimal(50,0), three varchar(10))"); + } + + @Test + @Override + public void testShowColumns() + { + // varchar length is different from base test + MaterializedResult actual = computeActual("SHOW COLUMNS FROM orders"); + + MaterializedResult expectedParametrizedVarchar = resultBuilder(getSession(), VARCHAR, VARCHAR, VARCHAR, VARCHAR) + .row("orderkey", "bigint", "", "") + .row("custkey", "bigint", "", "") + .row("orderstatus", "varchar(255)", "", "") + .row("totalprice", "double", "", "") + .row("orderdate", "date", "", "") + .row("orderpriority", "varchar(255)", "", "") + .row("clerk", "varchar(255)", "", "") + .row("shippriority", "integer", "", "") + .row("comment", "varchar(255)", "", "") + .build(); + + assertEquals(actual, expectedParametrizedVarchar); + } + + @Override + protected boolean isColumnNameRejected(Exception exception, String columnName, boolean delimited) + { + return nullToEmpty(exception.getMessage()).matches(".*(Incorrect column name).*"); + } + + @Override + protected Optional filterDataMappingSmokeTestData(DataMappingTestSetup dataMappingTestSetup) + { + String typeName = dataMappingTestSetup.getTrinoTypeName(); + if (typeName.equals("timestamp(3) with time zone") + || typeName.equals("timestamp")) { + return Optional.of(dataMappingTestSetup.asUnsupported()); + } + + if (typeName.equals("boolean")) { + // MariaDB does not have built-in support for boolean type. MariaDB provides BOOLEAN as the synonym of TINYINT(1) + // Querying the column with a boolean predicate subsequently fails with "Cannot apply operator: tinyint = boolean" + return Optional.empty(); + } + + return Optional.of(dataMappingTestSetup); + } + + @Test + @Override + public void testDescribeTable() + { + // varchar length is different from base test + MaterializedResult expectedColumns = resultBuilder(getSession(), VARCHAR, VARCHAR, VARCHAR, VARCHAR) + .row("orderkey", "bigint", "", "") + .row("custkey", "bigint", "", "") + .row("orderstatus", "varchar(255)", "", "") + .row("totalprice", "double", "", "") + .row("orderdate", "date", "", "") + .row("orderpriority", "varchar(255)", "", "") + .row("clerk", "varchar(255)", "", "") + .row("shippriority", "integer", "", "") + .row("comment", "varchar(255)", "", "") + .build(); + MaterializedResult actualColumns = computeActual("DESCRIBE orders"); + assertEquals(actualColumns, expectedColumns); + } + + @Test + @Override + public void testShowCreateTable() + { + // varchar length is different from base test + assertThat(computeActual("SHOW CREATE TABLE orders").getOnlyValue()) + .isEqualTo("CREATE TABLE mariadb.tpch.orders (\n" + + " orderkey bigint,\n" + + " custkey bigint,\n" + + " orderstatus varchar(255),\n" + + " totalprice double,\n" + + " orderdate date,\n" + + " orderpriority varchar(255),\n" + + " clerk varchar(255),\n" + + " shippriority integer,\n" + + " comment varchar(255)\n" + + ")"); + } + + @Test + public void testDropTable() + { + assertUpdate("CREATE TABLE test_drop AS SELECT 123 x", 1); + assertTrue(getQueryRunner().tableExists(getSession(), "test_drop")); + + assertUpdate("DROP TABLE test_drop"); + assertFalse(getQueryRunner().tableExists(getSession(), "test_drop")); + } + + @Test + public void testViews() + { + onRemoteDatabase().execute("CREATE OR REPLACE VIEW tpch.test_view AS SELECT * FROM tpch.orders"); + assertQuery("SELECT orderkey FROM test_view", "SELECT orderkey FROM orders"); + onRemoteDatabase().execute("DROP VIEW IF EXISTS tpch.test_view"); + } + + @Test + public void testColumnComment() + { + // TODO (https://github.com/trinodb/trino/issues/5333) add support for setting comments on existing column + + onRemoteDatabase().execute("CREATE TABLE tpch.test_column_comment (col1 bigint COMMENT 'test comment', col2 bigint COMMENT '', col3 bigint)"); + + assertQuery( + "SELECT column_name, comment FROM information_schema.columns WHERE table_schema = 'tpch' AND table_name = 'test_column_comment'", + "VALUES ('col1', 'test comment'), ('col2', null), ('col3', null)"); + + assertUpdate("DROP TABLE test_column_comment"); + } + + @Test + public void testPredicatePushdown() + { + // varchar equality + assertThat(query("SELECT regionkey, nationkey, name FROM nation WHERE name = 'ROMANIA'")) + .matches("VALUES (BIGINT '3', BIGINT '19', CAST('ROMANIA' AS varchar(255)))") + .isNotFullyPushedDown(FilterNode.class); + + // varchar range + assertThat(query("SELECT regionkey, nationkey, name FROM nation WHERE name BETWEEN 'POLAND' AND 'RPA'")) + .matches("VALUES (BIGINT '3', BIGINT '19', CAST('ROMANIA' AS varchar(255)))") + .isNotFullyPushedDown(FilterNode.class); + + // varchar different case + assertThat(query("SELECT regionkey, nationkey, name FROM nation WHERE name = 'romania'")) + .returnsEmptyResult() + .isNotFullyPushedDown(FilterNode.class); + + // bigint equality + assertThat(query("SELECT regionkey, nationkey, name FROM nation WHERE nationkey = 19")) + .matches("VALUES (BIGINT '3', BIGINT '19', CAST('ROMANIA' AS varchar(255)))") + .isFullyPushedDown(); + + // bigint range, with decimal to bigint simplification + assertThat(query("SELECT regionkey, nationkey, name FROM nation WHERE nationkey BETWEEN 18.5 AND 19.5")) + .matches("VALUES (BIGINT '3', BIGINT '19', CAST('ROMANIA' AS varchar(255)))") + .isFullyPushedDown(); + + // date equality + assertThat(query("SELECT orderkey FROM orders WHERE orderdate = DATE '1992-09-29'")) + .matches("VALUES BIGINT '1250', 34406, 38436, 57570") + .isFullyPushedDown(); + + onRemoteDatabase().execute("CREATE TABLE tpch.binary_test (x int, y varbinary(100))"); + onRemoteDatabase().execute("INSERT INTO tpch.binary_test VALUES (3, from_base64('AFCBhLrkidtNTZcA9Ru3hw=='))"); + + // varbinary equality + assertThat(query("SELECT x, y FROM tpch.binary_test WHERE y = from_base64('AFCBhLrkidtNTZcA9Ru3hw==')")) + .matches("VALUES (3, from_base64('AFCBhLrkidtNTZcA9Ru3hw=='))") + .isFullyPushedDown(); + + onRemoteDatabase().execute("DROP TABLE tpch.binary_test"); + + // predicate over aggregation key (likely to be optimized before being pushed down into the connector) + assertThat(query("SELECT * FROM (SELECT regionkey, sum(nationkey) FROM nation GROUP BY regionkey) WHERE regionkey = 3")) + .matches("VALUES (BIGINT '3', BIGINT '77')") + .isFullyPushedDown(); + + // predicate over aggregation result + assertThat(query("SELECT regionkey, sum(nationkey) FROM nation GROUP BY regionkey HAVING sum(nationkey) = 77")) + .matches("VALUES (BIGINT '3', BIGINT '77')") + .isFullyPushedDown(); + } + + @Test + @Override + public void testDeleteWithLike() + { + assertThatThrownBy(super::testDeleteWithLike) + .hasStackTraceContaining("TrinoException: Unsupported delete"); + } + + @Override + protected String errorMessageForCreateTableAsSelectNegativeDate(String date) + { + return format("Failed to insert data: \\(conn=.*\\) Incorrect date value: '%s'.*", date); + } + + @Override + protected String errorMessageForInsertNegativeDate(String date) + { + return format("Failed to insert data: \\(conn=.*\\) Incorrect date value: '%s'.*", date); + } + + @Override + protected String errorMessageForInsertIntoNotNullColumn(String columnName) + { + return format("Failed to insert data: \\(conn=.*\\) Field '%s' doesn't have a default value", columnName); + } +} diff --git a/plugin/trino-mariadb/src/test/java/io/trino/plugin/mariadb/MariaDbQueryRunner.java b/plugin/trino-mariadb/src/test/java/io/trino/plugin/mariadb/MariaDbQueryRunner.java new file mode 100644 index 000000000000..5cdde7f1916a --- /dev/null +++ b/plugin/trino-mariadb/src/test/java/io/trino/plugin/mariadb/MariaDbQueryRunner.java @@ -0,0 +1,90 @@ +/* + * 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.mariadb; + +import com.google.common.collect.ImmutableMap; +import io.airlift.log.Logger; +import io.trino.Session; +import io.trino.plugin.tpch.TpchPlugin; +import io.trino.testing.DistributedQueryRunner; +import io.trino.tpch.TpchTable; + +import java.util.HashMap; +import java.util.Map; + +import static io.airlift.testing.Closeables.closeAllSuppress; +import static io.trino.plugin.tpch.TpchMetadata.TINY_SCHEMA_NAME; +import static io.trino.testing.QueryAssertions.copyTpchTables; +import static io.trino.testing.TestingSession.testSessionBuilder; + +public final class MariaDbQueryRunner +{ + private static final String TPCH_SCHEMA = "tpch"; + + private MariaDbQueryRunner() {} + + public static DistributedQueryRunner createMariaDbQueryRunner( + TestingMariaDbServer server, + Map extraProperties, + Map connectorProperties, + Iterable> tables) + throws Exception + { + DistributedQueryRunner queryRunner = DistributedQueryRunner.builder(createSession()) + .setExtraProperties(extraProperties) + .build(); + try { + queryRunner.installPlugin(new TpchPlugin()); + queryRunner.createCatalog("tpch", "tpch"); + + connectorProperties = new HashMap<>(ImmutableMap.copyOf(connectorProperties)); + connectorProperties.putIfAbsent("connection-url", server.getJdbcUrl()); + connectorProperties.putIfAbsent("connection-user", server.getUsername()); + connectorProperties.putIfAbsent("connection-password", server.getPassword()); + + queryRunner.installPlugin(new MariaDbPlugin()); + queryRunner.createCatalog("mariadb", "mariadb", connectorProperties); + + copyTpchTables(queryRunner, "tpch", TINY_SCHEMA_NAME, createSession(), tables); + + return queryRunner; + } + catch (Throwable e) { + closeAllSuppress(e, queryRunner); + throw e; + } + } + + private static Session createSession() + { + return testSessionBuilder() + .setCatalog("mariadb") + .setSchema(TPCH_SCHEMA) + .build(); + } + + public static void main(String[] args) + throws Exception + { + DistributedQueryRunner queryRunner = createMariaDbQueryRunner( + new TestingMariaDbServer(), + ImmutableMap.of("http-server.http.port", "8080"), + ImmutableMap.of(), + TpchTable.getTables()); + + Logger log = Logger.get(MariaDbQueryRunner.class); + log.info("======== SERVER STARTED ========"); + log.info("\n====\n%s\n====", queryRunner.getCoordinator().getBaseUrl()); + } +} diff --git a/plugin/trino-mariadb/src/test/java/io/trino/plugin/mariadb/TestMariaDbCaseInsensitiveMapping.java b/plugin/trino-mariadb/src/test/java/io/trino/plugin/mariadb/TestMariaDbCaseInsensitiveMapping.java new file mode 100644 index 000000000000..44f61d10a73a --- /dev/null +++ b/plugin/trino-mariadb/src/test/java/io/trino/plugin/mariadb/TestMariaDbCaseInsensitiveMapping.java @@ -0,0 +1,82 @@ +/* + * 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.mariadb; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.trino.plugin.jdbc.BaseCaseInsensitiveMappingTest; +import io.trino.testing.QueryRunner; +import io.trino.testing.sql.SqlExecutor; +import org.testng.annotations.Test; + +import java.nio.file.Path; + +import static io.trino.plugin.jdbc.mapping.RuleBasedIdentifierMappingUtils.createRuleBasedIdentifierMappingFile; +import static io.trino.plugin.mariadb.MariaDbQueryRunner.createMariaDbQueryRunner; +import static java.util.Objects.requireNonNull; + +// With case-insensitive-name-matching enabled colliding schema/table names are considered as errors. +// Some tests here create colliding names which can cause any other concurrent test to fail. +@Test(singleThreaded = true) +public class TestMariaDbCaseInsensitiveMapping + extends BaseCaseInsensitiveMappingTest +{ + private Path mappingFile; + private TestingMariaDbServer server; + + @Override + protected QueryRunner createQueryRunner() + throws Exception + { + mappingFile = createRuleBasedIdentifierMappingFile(); + server = closeAfterClass(new TestingMariaDbServer()); + return createMariaDbQueryRunner( + server, + ImmutableMap.of(), + ImmutableMap.builder() + .put("case-insensitive-name-matching", "true") + .put("case-insensitive-name-matching.config-file", mappingFile.toFile().getAbsolutePath()) + .put("case-insensitive-name-matching.config-file.refresh-period", "1ms") // ~always refresh + .buildOrThrow(), + ImmutableList.of()); + } + + @Override + protected Path getMappingFile() + { + return requireNonNull(mappingFile, "mappingFile is null"); + } + + @Override + protected SqlExecutor onRemoteDatabase() + { + return requireNonNull(server, "server is null")::execute; + } + + @Override + protected String quoted(String name) + { + String identifierQuote = "`"; + name = name.replace(identifierQuote, identifierQuote + identifierQuote); + return identifierQuote + name + identifierQuote; + } + + @Test + public void forceTestNgToRespectSingleThreaded() + { + // TODO: Remove after updating TestNG to 7.4.0+ (https://github.com/trinodb/trino/issues/8571) + // TestNG doesn't enforce @Test(singleThreaded = true) when tests are defined in base class. According to + // https://github.com/cbeust/testng/issues/2361#issuecomment-688393166 a workaround it to add a dummy test to the leaf test class. + } +} diff --git a/plugin/trino-mariadb/src/test/java/io/trino/plugin/mariadb/TestMariaDbClient.java b/plugin/trino-mariadb/src/test/java/io/trino/plugin/mariadb/TestMariaDbClient.java new file mode 100644 index 000000000000..46965eaaffe7 --- /dev/null +++ b/plugin/trino-mariadb/src/test/java/io/trino/plugin/mariadb/TestMariaDbClient.java @@ -0,0 +1,158 @@ +/* + * 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.mariadb; + +import io.trino.plugin.jdbc.BaseJdbcConfig; +import io.trino.plugin.jdbc.ColumnMapping; +import io.trino.plugin.jdbc.DefaultQueryBuilder; +import io.trino.plugin.jdbc.JdbcClient; +import io.trino.plugin.jdbc.JdbcColumnHandle; +import io.trino.plugin.jdbc.JdbcExpression; +import io.trino.plugin.jdbc.JdbcTypeHandle; +import io.trino.plugin.jdbc.mapping.DefaultIdentifierMapping; +import io.trino.spi.connector.AggregateFunction; +import io.trino.spi.connector.ColumnHandle; +import io.trino.spi.expression.ConnectorExpression; +import io.trino.spi.expression.Variable; +import org.testng.annotations.Test; + +import java.sql.Types; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static io.trino.spi.type.BigintType.BIGINT; +import static io.trino.spi.type.BooleanType.BOOLEAN; +import static io.trino.spi.type.DoubleType.DOUBLE; +import static io.trino.testing.TestingConnectorSession.SESSION; +import static io.trino.testing.assertions.Assert.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; +import static org.testng.Assert.assertTrue; + +public class TestMariaDbClient +{ + private static final JdbcColumnHandle BIGINT_COLUMN = + JdbcColumnHandle.builder() + .setColumnName("c_bigint") + .setColumnType(BIGINT) + .setJdbcTypeHandle(new JdbcTypeHandle(Types.BIGINT, Optional.of("int8"), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty())) + .build(); + + private static final JdbcColumnHandle DOUBLE_COLUMN = + JdbcColumnHandle.builder() + .setColumnName("c_double") + .setColumnType(DOUBLE) + .setJdbcTypeHandle(new JdbcTypeHandle(Types.DOUBLE, Optional.of("double"), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty())) + .build(); + + private static final JdbcClient JDBC_CLIENT = new MariaDbClient( + new BaseJdbcConfig(), + session -> { + throw new UnsupportedOperationException(); + }, + new DefaultQueryBuilder(), + new DefaultIdentifierMapping()); + + @Test + public void testImplementCount() + { + Variable bigintVariable = new Variable("v_bigint", BIGINT); + Variable doubleVariable = new Variable("v_double", BIGINT); + Optional filter = Optional.of(new Variable("a_filter", BOOLEAN)); + + // count(*) + testImplementAggregation( + new AggregateFunction("count", BIGINT, List.of(), List.of(), false, Optional.empty()), + Map.of(), + Optional.of("count(*)")); + + // count(bigint) + testImplementAggregation( + new AggregateFunction("count", BIGINT, List.of(bigintVariable), List.of(), false, Optional.empty()), + Map.of(bigintVariable.getName(), BIGINT_COLUMN), + Optional.of("count(`c_bigint`)")); + + // count(double) + testImplementAggregation( + new AggregateFunction("count", BIGINT, List.of(doubleVariable), List.of(), false, Optional.empty()), + Map.of(doubleVariable.getName(), DOUBLE_COLUMN), + Optional.of("count(`c_double`)")); + + // count(DISTINCT bigint) + testImplementAggregation( + new AggregateFunction("count", BIGINT, List.of(bigintVariable), List.of(), true, Optional.empty()), + Map.of(bigintVariable.getName(), BIGINT_COLUMN), + Optional.empty()); + + // count() FILTER (WHERE ...) + + testImplementAggregation( + new AggregateFunction("count", BIGINT, List.of(), List.of(), false, filter), + Map.of(), + Optional.empty()); + + // count(bigint) FILTER (WHERE ...) + testImplementAggregation( + new AggregateFunction("count", BIGINT, List.of(bigintVariable), List.of(), false, filter), + Map.of(bigintVariable.getName(), BIGINT_COLUMN), + Optional.empty()); + } + + @Test + public void testImplementSum() + { + Variable bigintVariable = new Variable("v_bigint", BIGINT); + Variable doubleVariable = new Variable("v_double", DOUBLE); + Optional filter = Optional.of(new Variable("a_filter", BOOLEAN)); + + // sum(bigint) + testImplementAggregation( + new AggregateFunction("sum", BIGINT, List.of(bigintVariable), List.of(), false, Optional.empty()), + Map.of(bigintVariable.getName(), BIGINT_COLUMN), + Optional.of("sum(`c_bigint`)")); + + // sum(double) + testImplementAggregation( + new AggregateFunction("sum", DOUBLE, List.of(doubleVariable), List.of(), false, Optional.empty()), + Map.of(doubleVariable.getName(), DOUBLE_COLUMN), + Optional.of("sum(`c_double`)")); + + // sum(DISTINCT bigint) + testImplementAggregation( + new AggregateFunction("sum", BIGINT, List.of(bigintVariable), List.of(), true, Optional.empty()), + Map.of(bigintVariable.getName(), BIGINT_COLUMN), + Optional.empty()); // distinct not supported + + // sum(bigint) FILTER (WHERE ...) + testImplementAggregation( + new AggregateFunction("sum", BIGINT, List.of(bigintVariable), List.of(), false, filter), + Map.of(bigintVariable.getName(), BIGINT_COLUMN), + Optional.empty()); // filter not supported + } + + private static void testImplementAggregation(AggregateFunction aggregateFunction, Map assignments, Optional expectedExpression) + { + Optional result = JDBC_CLIENT.implementAggregation(SESSION, aggregateFunction, assignments); + if (expectedExpression.isEmpty()) { + assertThat(result).isEmpty(); + } + else { + assertThat(result).isPresent(); + assertEquals(result.get().getExpression(), expectedExpression.get()); + Optional columnMapping = JDBC_CLIENT.toColumnMapping(SESSION, null, result.get().getJdbcTypeHandle()); + assertTrue(columnMapping.isPresent(), "No mapping for: " + result.get().getJdbcTypeHandle()); + assertEquals(columnMapping.get().getType(), aggregateFunction.getOutputType()); + } + } +} diff --git a/plugin/trino-mariadb/src/test/java/io/trino/plugin/mariadb/TestMariaDbConnectorTest.java b/plugin/trino-mariadb/src/test/java/io/trino/plugin/mariadb/TestMariaDbConnectorTest.java new file mode 100644 index 000000000000..42db2c33a58e --- /dev/null +++ b/plugin/trino-mariadb/src/test/java/io/trino/plugin/mariadb/TestMariaDbConnectorTest.java @@ -0,0 +1,46 @@ +/* + * 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.mariadb; + +import com.google.common.collect.ImmutableMap; +import io.trino.testing.QueryRunner; +import io.trino.testing.sql.SqlExecutor; + +import static io.trino.plugin.mariadb.MariaDbQueryRunner.createMariaDbQueryRunner; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class TestMariaDbConnectorTest + extends BaseMariaDbConnectorTest +{ + @Override + protected QueryRunner createQueryRunner() + throws Exception + { + server = closeAfterClass(new TestingMariaDbServer()); + return createMariaDbQueryRunner(server, ImmutableMap.of(), ImmutableMap.of(), REQUIRED_TPCH_TABLES); + } + + @Override + protected SqlExecutor onRemoteDatabase() + { + return server::execute; + } + + @Override + public void testRenameColumn() + { + assertThatThrownBy(super::testRenameColumn) + .hasMessageContaining("Rename column not supported for the MariaDB server version"); + } +} diff --git a/plugin/trino-mariadb/src/test/java/io/trino/plugin/mariadb/TestMariaDbLatestConnectorTest.java b/plugin/trino-mariadb/src/test/java/io/trino/plugin/mariadb/TestMariaDbLatestConnectorTest.java new file mode 100644 index 000000000000..7664ef9e0cb3 --- /dev/null +++ b/plugin/trino-mariadb/src/test/java/io/trino/plugin/mariadb/TestMariaDbLatestConnectorTest.java @@ -0,0 +1,39 @@ +/* + * 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.mariadb; + +import com.google.common.collect.ImmutableMap; +import io.trino.testing.QueryRunner; +import io.trino.testing.sql.SqlExecutor; + +import static io.trino.plugin.mariadb.MariaDbQueryRunner.createMariaDbQueryRunner; +import static io.trino.plugin.mariadb.TestingMariaDbServer.LATEST_VERSION; + +public class TestMariaDbLatestConnectorTest + extends BaseMariaDbConnectorTest +{ + @Override + protected QueryRunner createQueryRunner() + throws Exception + { + server = closeAfterClass(new TestingMariaDbServer(LATEST_VERSION)); + return createMariaDbQueryRunner(server, ImmutableMap.of(), ImmutableMap.of(), REQUIRED_TPCH_TABLES); + } + + @Override + protected SqlExecutor onRemoteDatabase() + { + return server::execute; + } +} diff --git a/plugin/trino-mariadb/src/test/java/io/trino/plugin/mariadb/TestMariaDbPlugin.java b/plugin/trino-mariadb/src/test/java/io/trino/plugin/mariadb/TestMariaDbPlugin.java new file mode 100644 index 000000000000..6985b323c70d --- /dev/null +++ b/plugin/trino-mariadb/src/test/java/io/trino/plugin/mariadb/TestMariaDbPlugin.java @@ -0,0 +1,33 @@ +/* + * 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.mariadb; + +import com.google.common.collect.ImmutableMap; +import io.trino.spi.Plugin; +import io.trino.spi.connector.ConnectorFactory; +import io.trino.testing.TestingConnectorContext; +import org.testng.annotations.Test; + +import static com.google.common.collect.Iterables.getOnlyElement; + +public class TestMariaDbPlugin +{ + @Test + public void testCreateConnector() + { + Plugin plugin = new MariaDbPlugin(); + ConnectorFactory factory = getOnlyElement(plugin.getConnectorFactories()); + factory.create("test", ImmutableMap.of("connection-url", "jdbc:mariadb://test"), new TestingConnectorContext()).shutdown(); + } +} diff --git a/plugin/trino-mariadb/src/test/java/io/trino/plugin/mariadb/TestMariaDbTypeMapping.java b/plugin/trino-mariadb/src/test/java/io/trino/plugin/mariadb/TestMariaDbTypeMapping.java new file mode 100644 index 000000000000..494c5ff37183 --- /dev/null +++ b/plugin/trino-mariadb/src/test/java/io/trino/plugin/mariadb/TestMariaDbTypeMapping.java @@ -0,0 +1,771 @@ +/* + * 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.mariadb; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.trino.Session; +import io.trino.plugin.jdbc.UnsupportedTypeHandling; +import io.trino.spi.type.TimeZoneKey; +import io.trino.testing.AbstractTestQueryFramework; +import io.trino.testing.QueryRunner; +import io.trino.testing.TestingSession; +import io.trino.testing.datatype.CreateAndInsertDataSetup; +import io.trino.testing.datatype.CreateAsSelectDataSetup; +import io.trino.testing.datatype.DataSetup; +import io.trino.testing.datatype.SqlDataTypeTest; +import io.trino.testing.sql.SqlExecutor; +import io.trino.testing.sql.TestTable; +import io.trino.testing.sql.TrinoSqlExecutor; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.ZoneId; + +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Verify.verify; +import static io.trino.plugin.jdbc.DecimalConfig.DecimalMapping.ALLOW_OVERFLOW; +import static io.trino.plugin.jdbc.DecimalConfig.DecimalMapping.STRICT; +import static io.trino.plugin.jdbc.DecimalSessionSessionProperties.DECIMAL_DEFAULT_SCALE; +import static io.trino.plugin.jdbc.DecimalSessionSessionProperties.DECIMAL_MAPPING; +import static io.trino.plugin.jdbc.DecimalSessionSessionProperties.DECIMAL_ROUNDING_MODE; +import static io.trino.plugin.jdbc.TypeHandlingJdbcSessionProperties.UNSUPPORTED_TYPE_HANDLING; +import static io.trino.plugin.jdbc.UnsupportedTypeHandling.CONVERT_TO_VARCHAR; +import static io.trino.plugin.mariadb.MariaDbQueryRunner.createMariaDbQueryRunner; +import static io.trino.spi.type.BigintType.BIGINT; +import static io.trino.spi.type.CharType.createCharType; +import static io.trino.spi.type.DateType.DATE; +import static io.trino.spi.type.DecimalType.createDecimalType; +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.TimeType.createTimeType; +import static io.trino.spi.type.TinyintType.TINYINT; +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.String.format; +import static java.math.RoundingMode.HALF_UP; +import static java.math.RoundingMode.UNNECESSARY; +import static java.time.ZoneOffset.UTC; +import static java.util.Arrays.asList; + +/** + * @see MariaDB data types + */ +public class TestMariaDbTypeMapping + extends AbstractTestQueryFramework +{ + protected TestingMariaDbServer server; + + private final ZoneId jvmZone = ZoneId.systemDefault(); + // no DST in 1970, but has DST in later years (e.g. 2018) + private final ZoneId vilnius = ZoneId.of("Europe/Vilnius"); + // minutes offset change since 1970-01-01, no DST + private final ZoneId kathmandu = ZoneId.of("Asia/Kathmandu"); + + @BeforeClass + public void setUp() + { + checkState(jvmZone.getId().equals("America/Bahia_Banderas"), "This test assumes certain JVM time zone"); + checkIsGap(jvmZone, LocalDate.of(1970, 1, 1)); + checkIsGap(vilnius, LocalDate.of(1983, 4, 1)); + verify(vilnius.getRules().getValidOffsets(LocalDate.of(1983, 10, 1).atStartOfDay().minusMinutes(1)).size() == 2); + } + + @Override + protected QueryRunner createQueryRunner() + throws Exception + { + server = closeAfterClass(new TestingMariaDbServer()); + return createMariaDbQueryRunner(server, ImmutableMap.of(), ImmutableMap.of(), ImmutableList.of()); + } + + @Test + public void testBoolean() + { + SqlDataTypeTest.create() + .addRoundTrip("boolean", "true", TINYINT, "TINYINT '1'") + .addRoundTrip("boolean", "false", TINYINT, "TINYINT '0'") + .addRoundTrip("boolean", "NULL", TINYINT, "CAST(NULL AS TINYINT)") + .execute(getQueryRunner(), mariaDbCreateAndInsert("tpch.test_boolean")) + .execute(getQueryRunner(), trinoCreateAsSelect("tpch.test_boolean")); + + SqlDataTypeTest.create() + .addRoundTrip("tinyint(1)", "true", TINYINT, "TINYINT '1'") + .addRoundTrip("tinyint(1)", "false", TINYINT, "TINYINT '0'") + .addRoundTrip("tinyint(1)", "NULL", TINYINT, "CAST(NULL AS TINYINT)") + .execute(getQueryRunner(), mariaDbCreateAndInsert("tpch.test_boolean")); + } + + @Test + public void testTinyInt() + { + SqlDataTypeTest.create() + .addRoundTrip("tinyint", "-128", TINYINT, "TINYINT '-128'") + .addRoundTrip("tinyint", "127", TINYINT, "TINYINT '127'") + .addRoundTrip("tinyint", "NULL", TINYINT, "CAST(NULL AS TINYINT)") + .execute(getQueryRunner(), mariaDbCreateAndInsert("tpch.test_tinyint")) + .execute(getQueryRunner(), trinoCreateAsSelect("tpch.test_tinyint")); + } + + @Test + public void testTinyIntUnsigned() + { + SqlDataTypeTest.create() + .addRoundTrip("tinyint unsigned", "0", SMALLINT, "SMALLINT '0'") + .addRoundTrip("tinyint unsigned", "255", SMALLINT, "SMALLINT '255'") + .addRoundTrip("tinyint unsigned", "NULL", SMALLINT, "CAST(NULL AS SMALLINT)") + .execute(getQueryRunner(), mariaDbCreateAndInsert("tpch.test_tinyint")); + } + + @Test + public void testSmallInt() + { + SqlDataTypeTest.create() + .addRoundTrip("smallint", "-32768", SMALLINT, "SMALLINT '-32768'") + .addRoundTrip("smallint", "32767", SMALLINT, "SMALLINT '32767'") + .addRoundTrip("smallint", "NULL", SMALLINT, "CAST(NULL AS SMALLINT)") + .execute(getQueryRunner(), mariaDbCreateAndInsert("tpch.test_smallint")) + .execute(getQueryRunner(), trinoCreateAsSelect("tpch.test_smallint")); + } + + @Test + public void testSmallIntUnsigned() + { + SqlDataTypeTest.create() + .addRoundTrip("smallint unsigned", "0", INTEGER, "0") + .addRoundTrip("smallint unsigned", "65535", INTEGER, "65535") + .addRoundTrip("smallint unsigned", "NULL", INTEGER, "CAST(NULL AS INTEGER)") + .execute(getQueryRunner(), mariaDbCreateAndInsert("tpch.test_smallint_unsigned")); + } + + @Test + public void testMediumInt() + { + SqlDataTypeTest.create() + .addRoundTrip("integer", "-8388608", INTEGER, "-8388608") + .addRoundTrip("integer", "8388607", INTEGER, "8388607") + .addRoundTrip("integer", "NULL", INTEGER, "CAST(NULL AS INTEGER)") + .execute(getQueryRunner(), mariaDbCreateAndInsert("tpch.test_mediumint")) + .execute(getQueryRunner(), trinoCreateAsSelect("tpch.test_mediumint")); + } + + @Test + public void testMediumIntUnsigned() + { + SqlDataTypeTest.create() + .addRoundTrip("mediumint unsigned", "0", INTEGER, "0") + .addRoundTrip("mediumint unsigned", "16777215", INTEGER, "16777215") + .addRoundTrip("mediumint unsigned", "NULL", INTEGER, "CAST(NULL AS INTEGER)") + .execute(getQueryRunner(), mariaDbCreateAndInsert("tpch.test_mediumint_unsigned")); + } + + @Test + public void testInt() + { + SqlDataTypeTest.create() + .addRoundTrip("int", "-2147483648", INTEGER, "-2147483648") + .addRoundTrip("int", "2147483647", INTEGER, "2147483647") + .addRoundTrip("int", "NULL", INTEGER, "CAST(NULL AS INTEGER)") + .execute(getQueryRunner(), mariaDbCreateAndInsert("tpch.test_integer")) + .execute(getQueryRunner(), trinoCreateAsSelect("tpch.test_integer")); + } + + @Test + public void testIntUnsigned() + { + SqlDataTypeTest.create() + .addRoundTrip("int unsigned", "0", BIGINT, "BIGINT '0'") + .addRoundTrip("int unsigned", "4294967295", BIGINT, "BIGINT '4294967295'") + .addRoundTrip("int unsigned", "NULL", BIGINT, "CAST(NULL AS BIGINT)") + .execute(getQueryRunner(), mariaDbCreateAndInsert("tpch.test_integer_unsigned")); + } + + @Test + public void testBigInt() + { + SqlDataTypeTest.create() + .addRoundTrip("bigint", "-9223372036854775808", BIGINT, "-9223372036854775808") + .addRoundTrip("bigint", "9223372036854775807", BIGINT, "9223372036854775807") + .addRoundTrip("bigint", "NULL", BIGINT, "CAST(NULL AS BIGINT)") + .execute(getQueryRunner(), mariaDbCreateAndInsert("tpch.test_bigint")) + .execute(getQueryRunner(), trinoCreateAsSelect("tpch.test_bigint")); + } + + @Test + public void testBigIntUnsigned() + { + SqlDataTypeTest.create() + .addRoundTrip("bigint unsigned", "0", createDecimalType(20, 0), "CAST('0' AS DECIMAL(20,0))") + .addRoundTrip("bigint unsigned", "18446744073709551615", createDecimalType(20, 0), "DECIMAL '18446744073709551615'") + .addRoundTrip("bigint unsigned", "NULL", createDecimalType(20, 0), "CAST(NULL AS DECIMAL(20,0))") + .execute(getQueryRunner(), mariaDbCreateAndInsert("tpch.test_bigint_unsigned")); + } + + @Test + public void testTypeAliases() + { + SqlDataTypeTest.create() + .addRoundTrip("int1", "1", TINYINT, "TINYINT '1'") + .addRoundTrip("int2", "2", SMALLINT, "SMALLINT '2'") + .addRoundTrip("int3", "3", INTEGER, "3") // INT3 is a synonym for MEDIUMINT + .addRoundTrip("int4", "4", INTEGER, "4") // INT4 is a synonym for INT + .addRoundTrip("integer", "5", INTEGER, "5") // INTEGER is a synonym for INT + .addRoundTrip("int8", "8", BIGINT, "BIGINT '8'") // INT8 is a synonym for BIGINT + .execute(getQueryRunner(), mariaDbCreateAndInsert("tpch.test_synonym")); + } + + @Test + public void testDecimal() + { + SqlDataTypeTest.create() + .addRoundTrip("decimal(3, 0)", "NULL", createDecimalType(3, 0), "CAST(NULL AS decimal(3, 0))") + .addRoundTrip("decimal(3, 0)", "CAST('193' AS decimal(3, 0))", createDecimalType(3, 0), "CAST('193' AS decimal(3, 0))") + .addRoundTrip("decimal(3, 0)", "CAST('19' AS decimal(3, 0))", createDecimalType(3, 0), "CAST('19' AS decimal(3, 0))") + .addRoundTrip("decimal(3, 0)", "CAST('-193' AS decimal(3, 0))", createDecimalType(3, 0), "CAST('-193' AS decimal(3, 0))") + .addRoundTrip("decimal(3, 1)", "CAST('10.0' AS decimal(3, 1))", createDecimalType(3, 1), "CAST('10.0' AS decimal(3, 1))") + .addRoundTrip("decimal(3, 1)", "CAST('10.1' AS decimal(3, 1))", createDecimalType(3, 1), "CAST('10.1' AS decimal(3, 1))") + .addRoundTrip("decimal(3, 1)", "CAST('-10.1' AS decimal(3, 1))", createDecimalType(3, 1), "CAST('-10.1' AS decimal(3, 1))") + .addRoundTrip("decimal(4, 2)", "CAST('2' AS decimal(4, 2))", createDecimalType(4, 2), "CAST('2' AS decimal(4, 2))") + .addRoundTrip("decimal(4, 2)", "CAST('2.3' AS decimal(4, 2))", createDecimalType(4, 2), "CAST('2.3' AS decimal(4, 2))") + .addRoundTrip("decimal(24, 2)", "CAST('2' AS decimal(24, 2))", createDecimalType(24, 2), "CAST('2' AS decimal(24, 2))") + .addRoundTrip("decimal(24, 2)", "CAST('2.3' AS decimal(24, 2))", createDecimalType(24, 2), "CAST('2.3' AS decimal(24, 2))") + .addRoundTrip("decimal(24, 2)", "CAST('123456789.3' AS decimal(24, 2))", createDecimalType(24, 2), "CAST('123456789.3' AS decimal(24, 2))") + .addRoundTrip("decimal(24, 4)", "CAST('12345678901234567890.31' AS decimal(24, 4))", createDecimalType(24, 4), "CAST('12345678901234567890.31' AS decimal(24, 4))") + .addRoundTrip("decimal(30, 5)", "CAST('3141592653589793238462643.38327' AS decimal(30, 5))", createDecimalType(30, 5), "CAST('3141592653589793238462643.38327' AS decimal(30, 5))") + .addRoundTrip("decimal(30, 5)", "CAST('-3141592653589793238462643.38327' AS decimal(30, 5))", createDecimalType(30, 5), "CAST('-3141592653589793238462643.38327' AS decimal(30, 5))") + .addRoundTrip("decimal(38, 0)", "CAST('27182818284590452353602874713526624977' AS decimal(38, 0))", createDecimalType(38, 0), "CAST('27182818284590452353602874713526624977' AS decimal(38, 0))") + .addRoundTrip("decimal(38, 0)", "CAST('-27182818284590452353602874713526624977' AS decimal(38, 0))", createDecimalType(38, 0), "CAST('-27182818284590452353602874713526624977' AS decimal(38, 0))") + .addRoundTrip("decimal(38, 0)", "CAST(NULL AS decimal(38, 0))", createDecimalType(38, 0), "CAST(NULL AS decimal(38, 0))") + .execute(getQueryRunner(), mariaDbCreateAndInsert("tpch.test_decimal")) + .execute(getQueryRunner(), trinoCreateAsSelect("test_decimal")); + } + + @Test + public void testDecimalExceedingPrecisionMax() + { + testUnsupportedDataType("decimal(50,0)"); + } + + @Test + public void testDecimalExceedingPrecisionMaxWithExceedingIntegerValues() + { + try (TestTable testTable = new TestTable( + server::execute, + "tpch.test_exceeding_max_decimal", + "(d_col decimal(65,25))", + asList("1234567890123456789012345678901234567890.123456789", "-1234567890123456789012345678901234567890.123456789"))) { + assertQuery( + sessionWithDecimalMappingAllowOverflow(UNNECESSARY, 0), + format("SELECT column_name, data_type FROM information_schema.columns WHERE table_schema = 'tpch' AND table_schema||'.'||table_name = '%s'", testTable.getName()), + "VALUES ('d_col', 'decimal(38,0)')"); + assertQueryFails( + sessionWithDecimalMappingAllowOverflow(UNNECESSARY, 0), + "SELECT d_col FROM " + testTable.getName(), + "Rounding necessary"); + assertQueryFails( + sessionWithDecimalMappingAllowOverflow(HALF_UP, 0), + "SELECT d_col FROM " + testTable.getName(), + "Decimal overflow"); + assertQuery( + sessionWithDecimalMappingStrict(CONVERT_TO_VARCHAR), + format("SELECT column_name, data_type FROM information_schema.columns WHERE table_schema = 'tpch' AND table_schema||'.'||table_name = '%s'", testTable.getName()), + "VALUES ('d_col', 'varchar')"); + assertQuery( + sessionWithDecimalMappingStrict(CONVERT_TO_VARCHAR), + "SELECT d_col FROM " + testTable.getName(), + "VALUES ('1234567890123456789012345678901234567890.1234567890000000000000000'), ('-1234567890123456789012345678901234567890.1234567890000000000000000')"); + } + } + + @Test + public void testDecimalExceedingPrecisionMaxWithNonExceedingIntegerValues() + { + try (TestTable testTable = new TestTable( + server::execute, + "tpch.test_exceeding_max_decimal", + "(d_col decimal(60,20))", + asList("123456789012345678901234567890.123456789012345", "-123456789012345678901234567890.123456789012345"))) { + assertQuery( + sessionWithDecimalMappingAllowOverflow(UNNECESSARY, 0), + format("SELECT column_name, data_type FROM information_schema.columns WHERE table_schema = 'tpch' AND table_schema||'.'||table_name = '%s'", testTable.getName()), + "VALUES ('d_col', 'decimal(38,0)')"); + assertQueryFails( + sessionWithDecimalMappingAllowOverflow(UNNECESSARY, 0), + "SELECT d_col FROM " + testTable.getName(), + "Rounding necessary"); + assertQuery( + sessionWithDecimalMappingAllowOverflow(HALF_UP, 0), + "SELECT d_col FROM " + testTable.getName(), + "VALUES (123456789012345678901234567890), (-123456789012345678901234567890)"); + assertQuery( + sessionWithDecimalMappingAllowOverflow(UNNECESSARY, 8), + format("SELECT column_name, data_type FROM information_schema.columns WHERE table_schema = 'tpch' AND table_schema||'.'||table_name = '%s'", testTable.getName()), + "VALUES ('d_col', 'decimal(38,8)')"); + assertQueryFails( + sessionWithDecimalMappingAllowOverflow(UNNECESSARY, 8), + "SELECT d_col FROM " + testTable.getName(), + "Rounding necessary"); + assertQuery( + sessionWithDecimalMappingAllowOverflow(HALF_UP, 8), + "SELECT d_col FROM " + testTable.getName(), + "VALUES (123456789012345678901234567890.12345679), (-123456789012345678901234567890.12345679)"); + assertQuery( + sessionWithDecimalMappingAllowOverflow(HALF_UP, 22), + format("SELECT column_name, data_type FROM information_schema.columns WHERE table_schema = 'tpch' AND table_schema||'.'||table_name = '%s'", testTable.getName()), + "VALUES ('d_col', 'decimal(38,20)')"); + assertQueryFails( + sessionWithDecimalMappingAllowOverflow(HALF_UP, 20), + "SELECT d_col FROM " + testTable.getName(), + "Decimal overflow"); + assertQueryFails( + sessionWithDecimalMappingAllowOverflow(HALF_UP, 9), + "SELECT d_col FROM " + testTable.getName(), + "Decimal overflow"); + assertQuery( + sessionWithDecimalMappingStrict(CONVERT_TO_VARCHAR), + format("SELECT column_name, data_type FROM information_schema.columns WHERE table_schema = 'tpch' AND table_schema||'.'||table_name = '%s'", testTable.getName()), + "VALUES ('d_col', 'varchar')"); + assertQuery( + sessionWithDecimalMappingStrict(CONVERT_TO_VARCHAR), + "SELECT d_col FROM " + testTable.getName(), + "VALUES ('123456789012345678901234567890.12345678901234500000'), ('-123456789012345678901234567890.12345678901234500000')"); + } + } + + @Test(dataProvider = "testDecimalExceedingPrecisionMaxProvider") + public void testDecimalExceedingPrecisionMaxWithSupportedValues(int typePrecision, int typeScale) + { + try (TestTable testTable = new TestTable( + server::execute, + "tpch.test_exceeding_max_decimal", + format("(d_col decimal(%d,%d))", typePrecision, typeScale), + asList("12.01", "-12.01", "123", "-123", "1.12345678", "-1.12345678"))) { + assertQuery( + sessionWithDecimalMappingAllowOverflow(UNNECESSARY, 0), + format("SELECT column_name, data_type FROM information_schema.columns WHERE table_schema = 'tpch' AND table_schema||'.'||table_name = '%s'", testTable.getName()), + "VALUES ('d_col', 'decimal(38,0)')"); + assertQueryFails( + sessionWithDecimalMappingAllowOverflow(UNNECESSARY, 0), + "SELECT d_col FROM " + testTable.getName(), + "Rounding necessary"); + assertQuery( + sessionWithDecimalMappingAllowOverflow(HALF_UP, 0), + "SELECT d_col FROM " + testTable.getName(), + "VALUES (12), (-12), (123), (-123), (1), (-1)"); + assertQuery( + sessionWithDecimalMappingAllowOverflow(HALF_UP, 3), + format("SELECT column_name, data_type FROM information_schema.columns WHERE table_schema = 'tpch' AND table_schema||'.'||table_name = '%s'", testTable.getName()), + "VALUES ('d_col', 'decimal(38,3)')"); + assertQuery( + sessionWithDecimalMappingAllowOverflow(HALF_UP, 3), + "SELECT d_col FROM " + testTable.getName(), + "VALUES (12.01), (-12.01), (123), (-123), (1.123), (-1.123)"); + assertQueryFails( + sessionWithDecimalMappingAllowOverflow(UNNECESSARY, 3), + "SELECT d_col FROM " + testTable.getName(), + "Rounding necessary"); + assertQuery( + sessionWithDecimalMappingAllowOverflow(HALF_UP, 8), + format("SELECT column_name, data_type FROM information_schema.columns WHERE table_schema = 'tpch' AND table_schema||'.'||table_name = '%s'", testTable.getName()), + "VALUES ('d_col', 'decimal(38,8)')"); + assertQuery( + sessionWithDecimalMappingAllowOverflow(HALF_UP, 8), + "SELECT d_col FROM " + testTable.getName(), + "VALUES (12.01), (-12.01), (123), (-123), (1.12345678), (-1.12345678)"); + assertQuery( + sessionWithDecimalMappingAllowOverflow(HALF_UP, 9), + "SELECT d_col FROM " + testTable.getName(), + "VALUES (12.01), (-12.01), (123), (-123), (1.12345678), (-1.12345678)"); + assertQuery( + sessionWithDecimalMappingAllowOverflow(UNNECESSARY, 8), + "SELECT d_col FROM " + testTable.getName(), + "VALUES (12.01), (-12.01), (123), (-123), (1.12345678), (-1.12345678)"); + } + } + + @DataProvider + public Object[][] testDecimalExceedingPrecisionMaxProvider() + { + return new Object[][] { + {40, 8}, + {50, 10}, + }; + } + + private Session sessionWithDecimalMappingAllowOverflow(RoundingMode roundingMode, int scale) + { + return Session.builder(getSession()) + .setCatalogSessionProperty("mariadb", DECIMAL_MAPPING, ALLOW_OVERFLOW.name()) + .setCatalogSessionProperty("mariadb", DECIMAL_ROUNDING_MODE, roundingMode.name()) + .setCatalogSessionProperty("mariadb", DECIMAL_DEFAULT_SCALE, Integer.valueOf(scale).toString()) + .build(); + } + + private Session sessionWithDecimalMappingStrict(UnsupportedTypeHandling unsupportedTypeHandling) + { + return Session.builder(getSession()) + .setCatalogSessionProperty("mariadb", DECIMAL_MAPPING, STRICT.name()) + .setCatalogSessionProperty("mariadb", UNSUPPORTED_TYPE_HANDLING, unsupportedTypeHandling.name()) + .build(); + } + + @Test + public void testFloat() + { + // we are not testing Nan/-Infinity/+Infinity as those are not supported by MariaDB + SqlDataTypeTest.create() + .addRoundTrip("real", "3.14", REAL, "REAL '3.14'") + .addRoundTrip("real", "10.3e0", REAL, "REAL '10.3e0'") + .addRoundTrip("real", "NULL", REAL, "CAST(NULL AS REAL)") + // .addRoundTrip("real", "3.1415927", REAL, "REAL '3.1415927'") // Overeagerly rounded by MariaDB to 3.14159 + .execute(getQueryRunner(), trinoCreateAsSelect("tpch.test_real")); + + SqlDataTypeTest.create() + .addRoundTrip("float", "3.14", REAL, "REAL '3.14'") + .addRoundTrip("float", "10.3e0", REAL, "REAL '10.3e0'") + .addRoundTrip("float", "NULL", REAL, "CAST(NULL AS REAL)") + // .addRoundTrip("real", "3.1415927", REAL, "REAL '3.1415927'") // Overeagerly rounded by MariaDB to 3.14159 + .execute(getQueryRunner(), mariaDbCreateAndInsert("tpch.test_float")); + } + + @Test + public void testDouble() + { + // we are not testing Nan/-Infinity/+Infinity as those are not supported by MariaDB + SqlDataTypeTest.create() + .addRoundTrip("double", "3.14", DOUBLE, "CAST(3.14 AS DOUBLE)") + .addRoundTrip("double", "1.0E100", DOUBLE, "1.0E100") + .addRoundTrip("double", "1.23456E12", DOUBLE, "1.23456E12") + .addRoundTrip("double", "NULL", DOUBLE, "CAST(NULL AS DOUBLE)") + .execute(getQueryRunner(), trinoCreateAsSelect("trino_test_double")) + .execute(getQueryRunner(), mariaDbCreateAndInsert("tpch.test_double")); + } + + @Test + public void testTrinoCreatedParameterizedVarchar() + { + SqlDataTypeTest.create() + .addRoundTrip("varchar(10)", "'text_a'", createVarcharType(255), "CAST('text_a' AS VARCHAR(255))") + .addRoundTrip("varchar(255)", "'text_b'", createVarcharType(255), "CAST('text_b' AS VARCHAR(255))") + .addRoundTrip("varchar(256)", "'text_c'", createVarcharType(65535), "CAST('text_c' AS VARCHAR(65535))") + .addRoundTrip("varchar(65535)", "'text_d'", createVarcharType(65535), "CAST('text_d' AS VARCHAR(65535))") + .addRoundTrip("varchar(65536)", "'text_e'", createVarcharType(16777215), "CAST('text_e' AS VARCHAR(16777215))") + .addRoundTrip("varchar(16777215)", "'text_f'", createVarcharType(16777215), "CAST('text_f' AS VARCHAR(16777215))") + .addRoundTrip("varchar(16777216)", "'text_g'", createUnboundedVarcharType(), "CAST('text_g' AS VARCHAR)") + .addRoundTrip("varchar(2147483646)", "'text_h'", createUnboundedVarcharType(), "CAST('text_h' AS VARCHAR)") + .addRoundTrip("varchar", "'unbounded'", createUnboundedVarcharType(), "CAST('unbounded' AS VARCHAR)") + .execute(getQueryRunner(), trinoCreateAsSelect("trino_test_parameterized_varchar")); + } + + @Test + public void testMariaDbCreatedParameterizedVarchar() + { + SqlDataTypeTest.create() + .addRoundTrip("tinytext", "'a'", createVarcharType(255), "CAST('a' AS VARCHAR(255))") + .addRoundTrip("text", "'b'", createVarcharType(65535), "CAST('b' AS VARCHAR(65535))") + .addRoundTrip("mediumtext", "'c'", createVarcharType(16777215), "CAST('c' AS VARCHAR(16777215))") + .addRoundTrip("longtext", "'d'", createUnboundedVarcharType(), "CAST('d' AS VARCHAR)") + .addRoundTrip("varchar(32)", "'e'", createVarcharType(32), "CAST('e' AS VARCHAR(32))") + .addRoundTrip("varchar(15000)", "'f'", createVarcharType(15000), "CAST('f' AS VARCHAR(15000))") + .execute(getQueryRunner(), mariaDbCreateAndInsert("tpch.mariadb_test_parameterized_varchar")); + } + + @Test + public void testMariaDbCreatedParameterizedVarcharUnicode() + { + SqlDataTypeTest.create() + .addRoundTrip("tinytext CHARACTER SET utf8", "'攻殻機動隊'", createVarcharType(255), "CAST('攻殻機動隊' AS VARCHAR(255))") + .addRoundTrip("text CHARACTER SET utf8", "'攻殻機動隊'", createVarcharType(65535), "CAST('攻殻機動隊' AS VARCHAR(65535))") + .addRoundTrip("mediumtext CHARACTER SET utf8", "'攻殻機動隊'", createVarcharType(16777215), "CAST('攻殻機動隊' AS VARCHAR(16777215))") + .addRoundTrip("longtext CHARACTER SET utf8", "'攻殻機動隊'", createUnboundedVarcharType(), "CAST('攻殻機動隊' AS VARCHAR)") + .addRoundTrip("varchar(5) CHARACTER SET utf8", "'攻殻機動隊'", createVarcharType(5), "CAST('攻殻機動隊' AS VARCHAR(5))") + .addRoundTrip("varchar(32) CHARACTER SET utf8", "'攻殻機動隊'", createVarcharType(32), "CAST('攻殻機動隊' AS VARCHAR(32))") + .addRoundTrip("varchar(20000) CHARACTER SET utf8", "'攻殻機動隊'", createVarcharType(20000), "CAST('攻殻機動隊' AS VARCHAR(20000))") + .addRoundTrip("varchar(1) CHARACTER SET utf8mb4", "'😂'", createVarcharType(1), "CAST('😂' AS VARCHAR(1))") + .addRoundTrip("varchar(77) CHARACTER SET utf8mb4", "'Ну, погоди!'", createVarcharType(77), "CAST('Ну, погоди!' AS VARCHAR(77))") + .execute(getQueryRunner(), mariaDbCreateAndInsert("tpch.mariadb_test_parameterized_varchar_unicode")); + } + + @Test + public void testParameterizedChar() + { + SqlDataTypeTest.create() + .addRoundTrip("char", "''", createCharType(1), "CAST('' AS CHAR(1))") + .addRoundTrip("char", "'a'", createCharType(1), "CAST('a' AS CHAR(1))") + .addRoundTrip("char(1)", "''", createCharType(1), "CAST('' AS CHAR(1))") + .addRoundTrip("char(1)", "'a'", createCharType(1), "CAST('a' AS CHAR(1))") + .addRoundTrip("char(8)", "'abc'", createCharType(8), "CAST('abc' AS CHAR(8))") + .addRoundTrip("char(8)", "'12345678'", createCharType(8), "CAST('12345678' AS CHAR(8))") + .execute(getQueryRunner(), trinoCreateAsSelect("mariadb_test_parameterized_char")) + .execute(getQueryRunner(), mariaDbCreateAndInsert("tpch.mariadb_test_parameterized_char")); + } + + @Test + public void testMariaDbParameterizedCharUnicode() + { + SqlDataTypeTest.create() + .addRoundTrip("char(1) CHARACTER SET utf8", "'攻'", createCharType(1), "CAST('攻' AS CHAR(1))") + .addRoundTrip("char(5) CHARACTER SET utf8", "'攻殻'", createCharType(5), "CAST('攻殻' AS CHAR(5))") + .addRoundTrip("char(5) CHARACTER SET utf8", "'攻殻機動隊'", createCharType(5), "CAST('攻殻機動隊' AS CHAR(5))") + .addRoundTrip("char(1)", "'😂'", createCharType(1), "CAST('😂' AS char(1))") + .addRoundTrip("char(77)", "'Ну, погоди!'", createCharType(77), "CAST('Ну, погоди!' AS char(77))") + .execute(getQueryRunner(), mariaDbCreateAndInsert("tpch.mariadb_test_parameterized_char")); + } + + @Test + public void testCharTrailingSpace() + { + SqlDataTypeTest.create() + .addRoundTrip("char(10)", "'test'", createCharType(10), "CAST('test' AS CHAR(10))") + .addRoundTrip("char(10)", "'test '", createCharType(10), "CAST('test' AS CHAR(10))") + .addRoundTrip("char(10)", "'test '", createCharType(10), "CAST('test' AS CHAR(10))") + .execute(getQueryRunner(), mariaDbCreateAndInsert("tpch.mariadb_char_trailing_space")); + } + + @Test + public void testVarbinary() + { + varbinaryTestCases("varbinary(50)") + .execute(getQueryRunner(), mariaDbCreateAndInsert("tpch.test_varbinary")); + + varbinaryTestCases("tinyblob") + .execute(getQueryRunner(), mariaDbCreateAndInsert("tpch.test_varbinary")); + + varbinaryTestCases("blob") + .execute(getQueryRunner(), mariaDbCreateAndInsert("tpch.test_varbinary")); + + varbinaryTestCases("mediumblob") + .execute(getQueryRunner(), mariaDbCreateAndInsert("tpch.test_varbinary")); + + varbinaryTestCases("longblob") + .execute(getQueryRunner(), mariaDbCreateAndInsert("tpch.test_varbinary")); + + varbinaryTestCases("varbinary") + .execute(getQueryRunner(), trinoCreateAsSelect("test_varbinary")); + } + + private SqlDataTypeTest varbinaryTestCases(String insertType) + { + return SqlDataTypeTest.create() + .addRoundTrip(insertType, "NULL", VARBINARY, "CAST(NULL AS varbinary)") + .addRoundTrip(insertType, "X''", VARBINARY, "X''") + .addRoundTrip(insertType, "X'68656C6C6F'", VARBINARY, "to_utf8('hello')") + .addRoundTrip(insertType, "X'5069C4996B6E6120C582C4856B61207720E69DB1E4BAACE983BD'", VARBINARY, "to_utf8('Piękna łąka w 東京都')") + .addRoundTrip(insertType, "X'4261672066756C6C206F6620F09F92B0'", VARBINARY, "to_utf8('Bag full of 💰')") + .addRoundTrip(insertType, "X'0001020304050607080DF9367AA7000000'", VARBINARY, "X'0001020304050607080DF9367AA7000000'") // non-text + .addRoundTrip(insertType, "X'000000000000'", VARBINARY, "X'000000000000'"); + } + + @Test + public void testBinary() + { + SqlDataTypeTest.create() + .addRoundTrip("binary(18)", "NULL", VARBINARY, "CAST(NULL AS varbinary)") + .addRoundTrip("binary(18)", "X''", VARBINARY, "X'000000000000000000000000000000000000'") + .addRoundTrip("binary(18)", "X'68656C6C6F'", VARBINARY, "to_utf8('hello') || X'00000000000000000000000000'") + .addRoundTrip("binary(18)", "X'C582C4856B61207720E69DB1E4BAACE983BD'", VARBINARY, "to_utf8('łąka w 東京都')") // no trailing zeros + .addRoundTrip("binary(18)", "X'4261672066756C6C206F6620F09F92B0'", VARBINARY, "to_utf8('Bag full of 💰') || X'0000'") + .addRoundTrip("binary(18)", "X'0001020304050607080DF9367AA7000000'", VARBINARY, "X'0001020304050607080DF9367AA700000000'") // non-text prefix + .addRoundTrip("binary(18)", "X'000000000000'", VARBINARY, "X'000000000000000000000000000000000000'") + .execute(getQueryRunner(), mariaDbCreateAndInsert("tpch.test_binary")); + } + + @Test(dataProvider = "sessionZonesDataProvider") + public void testDate(ZoneId sessionZone) + { + Session session = Session.builder(getSession()) + .setTimeZoneKey(TimeZoneKey.getTimeZoneKey(sessionZone.getId())) + .build(); + + SqlDataTypeTest.create() + .addRoundTrip("date", "NULL", DATE, "CAST(NULL AS DATE)") + .addRoundTrip("date", "DATE '0001-01-01'", DATE, "DATE '0001-01-01'") + .addRoundTrip("date", "DATE '0012-12-12'", DATE, "DATE '0012-12-12'") + .addRoundTrip("date", "DATE '1000-01-01'", DATE, "DATE '1000-01-01'") // min supported date in MariaDB + .addRoundTrip("date", "DATE '1500-01-01'", DATE, "DATE '1500-01-01'") + .addRoundTrip("date", "DATE '1582-10-05'", DATE, "DATE '1582-10-05'") // begin julian->gregorian switch + .addRoundTrip("date", "DATE '1582-10-14'", DATE, "DATE '1582-10-14'") // end julian->gregorian switch + .addRoundTrip("date", "DATE '1952-04-03'", DATE, "DATE '1952-04-03'") + .addRoundTrip("date", "DATE '1970-01-01'", DATE, "DATE '1970-01-01'") + .addRoundTrip("date", "DATE '1970-02-03'", DATE, "DATE '1970-02-03'") + .addRoundTrip("date", "DATE '1983-04-01'", DATE, "DATE '1983-04-01'") + .addRoundTrip("date", "DATE '1983-10-01'", DATE, "DATE '1983-10-01'") + .addRoundTrip("date", "DATE '2017-07-01'", DATE, "DATE '2017-07-01'") // summer on northern hemisphere (possible DST) + .addRoundTrip("date", "DATE '2017-01-01'", DATE, "DATE '2017-01-01'") // winter on northern hemisphere (possible DST on southern hemisphere) + .addRoundTrip("date", "DATE '9999-12-31'", DATE, "DATE '9999-12-31'") // max supported date in MariaDB + .execute(getQueryRunner(), session, trinoCreateAsSelect("test_date")) + .execute(getQueryRunner(), session, mariaDbCreateAndInsert("tpch.test_date")); + } + + @Test + public void testUnsupportedDate() + { + try (TestTable table = new TestTable(getQueryRunner()::execute, "test_negative_date", "(dt DATE)")) { + assertQueryFails(format("INSERT INTO %s VALUES (DATE '-0001-01-01')", table.getName()), ".*Failed to insert data.*"); + assertQueryFails(format("INSERT INTO %s VALUES (DATE '10000-01-01')", table.getName()), ".*Failed to insert data.*"); + } + } + + @Test + public void testTimeFromMariaDb() + { + SqlDataTypeTest.create() + // default precision in MariaDB is 0 + .addRoundTrip("TIME", "TIME '00:00:00'", createTimeType(0), "TIME '00:00:00'") + .addRoundTrip("TIME", "TIME '12:34:56'", createTimeType(0), "TIME '12:34:56'") + .addRoundTrip("TIME", "TIME '23:59:59'", createTimeType(0), "TIME '23:59:59'") + + // maximal value for a precision + .addRoundTrip("TIME(1)", "TIME '23:59:59.9'", createTimeType(1), "TIME '23:59:59.9'") + .addRoundTrip("TIME(2)", "TIME '23:59:59.99'", createTimeType(2), "TIME '23:59:59.99'") + .addRoundTrip("TIME(3)", "TIME '23:59:59.999'", createTimeType(3), "TIME '23:59:59.999'") + .addRoundTrip("TIME(4)", "TIME '23:59:59.9999'", createTimeType(4), "TIME '23:59:59.9999'") + .addRoundTrip("TIME(5)", "TIME '23:59:59.99999'", createTimeType(5), "TIME '23:59:59.99999'") + .addRoundTrip("TIME(6)", "TIME '23:59:59.999999'", createTimeType(6), "TIME '23:59:59.999999'") + .execute(getQueryRunner(), mariaDbCreateAndInsert("tpch.test_time")); + } + + @Test + public void testTimeFromTrino() + { + SqlDataTypeTest.create() + // default precision in Trino is 3 + .addRoundTrip("TIME", "TIME '00:00:00'", createTimeType(3), "TIME '00:00:00.000'") + .addRoundTrip("TIME", "TIME '12:34:56.123'", createTimeType(3), "TIME '12:34:56.123'") + .addRoundTrip("TIME", "TIME '23:59:59.999'", createTimeType(3), "TIME '23:59:59.999'") + + // maximal value for a precision + .addRoundTrip("TIME", "TIME '23:59:59'", createTimeType(3), "TIME '23:59:59.000'") + .addRoundTrip("TIME(1)", "TIME '23:59:59.9'", createTimeType(1), "TIME '23:59:59.9'") + .addRoundTrip("TIME(2)", "TIME '23:59:59.99'", createTimeType(2), "TIME '23:59:59.99'") + .addRoundTrip("TIME(3)", "TIME '23:59:59.999'", createTimeType(3), "TIME '23:59:59.999'") + .addRoundTrip("TIME(4)", "TIME '23:59:59.9999'", createTimeType(4), "TIME '23:59:59.9999'") + .addRoundTrip("TIME(5)", "TIME '23:59:59.99999'", createTimeType(5), "TIME '23:59:59.99999'") + .addRoundTrip("TIME(6)", "TIME '23:59:59.999999'", createTimeType(6), "TIME '23:59:59.999999'") + + // supported precisions + .addRoundTrip("TIME '23:59:59.9'", "TIME '23:59:59.9'") + .addRoundTrip("TIME '23:59:59.99'", "TIME '23:59:59.99'") + .addRoundTrip("TIME '23:59:59.999'", "TIME '23:59:59.999'") + .addRoundTrip("TIME '23:59:59.9999'", "TIME '23:59:59.9999'") + .addRoundTrip("TIME '23:59:59.99999'", "TIME '23:59:59.99999'") + .addRoundTrip("TIME '23:59:59.999999'", "TIME '23:59:59.999999'") + + // round down + .addRoundTrip("TIME '00:00:00.0000001'", "TIME '00:00:00.000000'") + .addRoundTrip("TIME '00:00:00.000000000001'", "TIME '00:00:00.000000'") + .addRoundTrip("TIME '12:34:56.1234561'", "TIME '12:34:56.123456'") + .addRoundTrip("TIME '23:59:59.9999994'", "TIME '23:59:59.999999'") + .addRoundTrip("TIME '23:59:59.999999499999'", "TIME '23:59:59.999999'") + + // round down, maximal value + .addRoundTrip("TIME '00:00:00.0000004'", "TIME '00:00:00.000000'") + .addRoundTrip("TIME '00:00:00.00000049'", "TIME '00:00:00.000000'") + .addRoundTrip("TIME '00:00:00.000000449'", "TIME '00:00:00.000000'") + .addRoundTrip("TIME '00:00:00.0000004449'", "TIME '00:00:00.000000'") + .addRoundTrip("TIME '00:00:00.00000044449'", "TIME '00:00:00.000000'") + .addRoundTrip("TIME '00:00:00.000000444449'", "TIME '00:00:00.000000'") + + // round up, minimal value + .addRoundTrip("TIME '00:00:00.0000005'", "TIME '00:00:00.000001'") + .addRoundTrip("TIME '00:00:00.00000050'", "TIME '00:00:00.000001'") + .addRoundTrip("TIME '00:00:00.000000500'", "TIME '00:00:00.000001'") + .addRoundTrip("TIME '00:00:00.0000005000'", "TIME '00:00:00.000001'") + .addRoundTrip("TIME '00:00:00.00000050000'", "TIME '00:00:00.000001'") + .addRoundTrip("TIME '00:00:00.000000500000'", "TIME '00:00:00.000001'") + + // round up, maximal value + .addRoundTrip("TIME '00:00:00.0000009'", "TIME '00:00:00.000001'") + .addRoundTrip("TIME '00:00:00.00000099'", "TIME '00:00:00.000001'") + .addRoundTrip("TIME '00:00:00.000000999'", "TIME '00:00:00.000001'") + .addRoundTrip("TIME '00:00:00.0000009999'", "TIME '00:00:00.000001'") + .addRoundTrip("TIME '00:00:00.00000099999'", "TIME '00:00:00.000001'") + .addRoundTrip("TIME '00:00:00.000000999999'", "TIME '00:00:00.000001'") + + // round up to next day, minimal value + .addRoundTrip("TIME '23:59:59.9999995'", "TIME '00:00:00.000000'") + .addRoundTrip("TIME '23:59:59.99999950'", "TIME '00:00:00.000000'") + .addRoundTrip("TIME '23:59:59.999999500'", "TIME '00:00:00.000000'") + .addRoundTrip("TIME '23:59:59.9999995000'", "TIME '00:00:00.000000'") + .addRoundTrip("TIME '23:59:59.99999950000'", "TIME '00:00:00.000000'") + .addRoundTrip("TIME '23:59:59.999999500000'", "TIME '00:00:00.000000'") + + // round up to next day, maximal value + .addRoundTrip("TIME '23:59:59.9999999'", "TIME '00:00:00.000000'") + .addRoundTrip("TIME '23:59:59.99999999'", "TIME '00:00:00.000000'") + .addRoundTrip("TIME '23:59:59.999999999'", "TIME '00:00:00.000000'") + .addRoundTrip("TIME '23:59:59.9999999999'", "TIME '00:00:00.000000'") + .addRoundTrip("TIME '23:59:59.99999999999'", "TIME '00:00:00.000000'") + .addRoundTrip("TIME '23:59:59.999999999999'", "TIME '00:00:00.000000'") + + .execute(getQueryRunner(), trinoCreateAsSelect("tpch.test_time")); + } + + private void testUnsupportedDataType(String databaseDataType) + { + SqlExecutor jdbcSqlExecutor = server::execute; + jdbcSqlExecutor.execute(format("CREATE TABLE tpch.test_unsupported_data_type(supported_column varchar(5), unsupported_column %s)", databaseDataType)); + try { + assertQuery( + "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = 'tpch' AND TABLE_NAME = 'test_unsupported_data_type'", + "VALUES 'supported_column'"); // no 'unsupported_column' + } + finally { + jdbcSqlExecutor.execute("DROP TABLE tpch.test_unsupported_data_type"); + } + } + + @DataProvider + public Object[][] sessionZonesDataProvider() + { + return new Object[][] { + {UTC}, + {jvmZone}, + {vilnius}, + {kathmandu}, + {ZoneId.of(TestingSession.DEFAULT_TIME_ZONE_KEY.getId())}, + }; + } + + private DataSetup trinoCreateAsSelect(String tableNamePrefix) + { + return trinoCreateAsSelect(getSession(), tableNamePrefix); + } + + private DataSetup trinoCreateAsSelect(Session session, String tableNamePrefix) + { + return new CreateAsSelectDataSetup(new TrinoSqlExecutor(getQueryRunner(), session), tableNamePrefix); + } + + private DataSetup mariaDbCreateAndInsert(String tableNamePrefix) + { + return new CreateAndInsertDataSetup(server::execute, tableNamePrefix); + } + + private static void checkIsGap(ZoneId zone, LocalDate date) + { + verify(isGap(zone, date), "Expected %s to be a gap in %s", date, zone); + } + + private static boolean isGap(ZoneId zone, LocalDate date) + { + return zone.getRules().getValidOffsets(date.atStartOfDay()).isEmpty(); + } +} diff --git a/plugin/trino-mariadb/src/test/java/io/trino/plugin/mariadb/TestingMariaDbServer.java b/plugin/trino-mariadb/src/test/java/io/trino/plugin/mariadb/TestingMariaDbServer.java new file mode 100644 index 000000000000..966695875961 --- /dev/null +++ b/plugin/trino-mariadb/src/test/java/io/trino/plugin/mariadb/TestingMariaDbServer.java @@ -0,0 +1,85 @@ +/* + * 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.mariadb; + +import org.testcontainers.containers.MariaDBContainer; +import org.testcontainers.utility.DockerImageName; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; + +import static java.lang.String.format; + +public class TestingMariaDbServer + implements AutoCloseable +{ + public static final String LATEST_VERSION = "10.7.1"; + public static final String DEFAULT_VERSION = "10.2"; + private static final int MARIADB_PORT = 3306; + + private final MariaDBContainer container; + + public TestingMariaDbServer() + { + this(DEFAULT_VERSION); + } + + public TestingMariaDbServer(String tag) + { + container = new MariaDBContainer<>(DockerImageName.parse("mariadb").withTag(tag)) + .withDatabaseName("tpch"); + container.withCommand("--character-set-server", "utf8mb4"); // The default character set is latin1 + container.start(); + execute(format("GRANT ALL PRIVILEGES ON *.* TO '%s'", container.getUsername()), "root", container.getPassword()); + } + + public void execute(String sql) + { + execute(sql, getUsername(), getPassword()); + } + + private void execute(String sql, String user, String password) + { + try (Connection connection = DriverManager.getConnection(getJdbcUrl(), user, password); + Statement statement = connection.createStatement()) { + statement.execute(sql); + } + catch (SQLException e) { + throw new RuntimeException(e); + } + } + + public String getUsername() + { + return container.getUsername(); + } + + public String getPassword() + { + return container.getPassword(); + } + + public String getJdbcUrl() + { + return format("jdbc:mariadb://%s:%s", container.getContainerIpAddress(), container.getMappedPort(MARIADB_PORT)); + } + + @Override + public void close() + { + container.close(); + } +} diff --git a/pom.xml b/pom.xml index 0dfc80997b76..addc152405d1 100644 --- a/pom.xml +++ b/pom.xml @@ -130,6 +130,7 @@ plugin/trino-kinesis plugin/trino-kudu plugin/trino-local-file + plugin/trino-mariadb plugin/trino-memory plugin/trino-ml plugin/trino-mongodb @@ -343,6 +344,12 @@ ${project.version} + + io.trino + trino-mariadb + ${project.version} + + io.trino trino-matching @@ -1600,6 +1607,13 @@ + + org.mariadb.jdbc + mariadb-java-client + + 2.7.5 + + org.openjdk.jol jol-core diff --git a/testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/env/environment/EnvMultinodeAllConnectors.java b/testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/env/environment/EnvMultinodeAllConnectors.java index ee8e8c3b8a7d..576e048126b3 100644 --- a/testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/env/environment/EnvMultinodeAllConnectors.java +++ b/testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/env/environment/EnvMultinodeAllConnectors.java @@ -62,6 +62,7 @@ public void extendEnvironment(Environment.Builder builder) "kinesis", "kudu", "localfile", + "mariadb", "memory", "memsql", "mongodb", diff --git a/testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/env/environment/EnvMultinodeMariadb.java b/testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/env/environment/EnvMultinodeMariadb.java new file mode 100644 index 000000000000..95294ba24de6 --- /dev/null +++ b/testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/env/environment/EnvMultinodeMariadb.java @@ -0,0 +1,74 @@ +/* + * 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.tests.product.launcher.env.environment; + +import io.trino.tests.product.launcher.docker.DockerFiles; +import io.trino.tests.product.launcher.docker.DockerFiles.ResourceProvider; +import io.trino.tests.product.launcher.env.DockerContainer; +import io.trino.tests.product.launcher.env.Environment.Builder; +import io.trino.tests.product.launcher.env.EnvironmentProvider; +import io.trino.tests.product.launcher.env.common.StandardMultinode; +import io.trino.tests.product.launcher.env.common.TestsEnvironment; +import io.trino.tests.product.launcher.testcontainers.PortBinder; +import org.testcontainers.containers.startupcheck.IsRunningStartupCheckStrategy; + +import javax.inject.Inject; + +import static io.trino.tests.product.launcher.docker.ContainerUtil.forSelectedPorts; +import static io.trino.tests.product.launcher.env.EnvironmentContainers.configureTempto; +import static java.util.Objects.requireNonNull; +import static org.testcontainers.utility.MountableFile.forHostPath; + +@TestsEnvironment +public class EnvMultinodeMariadb + extends EnvironmentProvider +{ + // Use non-default MariaDB port to avoid conflicts with locally installed MariaDB if any. + public static final int MARIADB_PORT = 23306; + + private final ResourceProvider configDir; + private final PortBinder portBinder; + + @Inject + public EnvMultinodeMariadb(StandardMultinode standardMultinode, DockerFiles dockerFiles, PortBinder portBinder) + { + super(standardMultinode); + this.configDir = requireNonNull(dockerFiles, "dockerFiles is null").getDockerFilesHostDirectory("conf/environment/multinode-mariadb/"); + this.portBinder = requireNonNull(portBinder, "portBinder is null"); + } + + @Override + public void extendEnvironment(Builder builder) + { + builder.addConnector("mariadb", forHostPath(configDir.getPath("mariadb.properties"))); + builder.addContainer(createMariaDb()); + configureTempto(builder, configDir); + } + + private DockerContainer createMariaDb() + { + DockerContainer container = new DockerContainer("mariadb:10.7.1", "mariadb") + .withEnv("MYSQL_USER", "test") + .withEnv("MYSQL_PASSWORD", "test") + .withEnv("MYSQL_ROOT_PASSWORD", "test") + .withEnv("MYSQL_DATABASE", "test") + .withCommand("mysqld", "--port", Integer.toString(MARIADB_PORT), "--character-set-server", "utf8mb4") + .withStartupCheckStrategy(new IsRunningStartupCheckStrategy()) + .waitingFor(forSelectedPorts(MARIADB_PORT)); + + portBinder.exposePort(container, MARIADB_PORT); + + return container; + } +} diff --git a/testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/suite/suites/Suite7NonGeneric.java b/testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/suite/suites/Suite7NonGeneric.java index 485ef53ad11b..358710df55fe 100644 --- a/testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/suite/suites/Suite7NonGeneric.java +++ b/testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/suite/suites/Suite7NonGeneric.java @@ -18,6 +18,7 @@ import io.trino.tests.product.launcher.env.EnvironmentDefaults; import io.trino.tests.product.launcher.env.environment.EnvMultinodeClickhouse; import io.trino.tests.product.launcher.env.environment.EnvMultinodeKerberosKudu; +import io.trino.tests.product.launcher.env.environment.EnvMultinodeMariadb; import io.trino.tests.product.launcher.env.environment.EnvSinglenodeHiveIcebergRedirections; import io.trino.tests.product.launcher.env.environment.EnvSinglenodeKerberosHdfsImpersonationCrossRealm; import io.trino.tests.product.launcher.env.environment.EnvSinglenodeMysql; @@ -47,6 +48,9 @@ public List getTestRuns(EnvironmentConfig config) testOnEnvironment(EnvSinglenodeMysql.class) .withGroups("configured_features", "mysql") .build(), + testOnEnvironment(EnvMultinodeMariadb.class) + .withGroups("configured_features", "mariadb") + .build(), testOnEnvironment(EnvSinglenodePostgresql.class) .withGroups("configured_features", "postgresql") .build(), diff --git a/testing/trino-product-tests-launcher/src/main/resources/docker/presto-product-tests/conf/environment/multinode-all/mariadb.properties b/testing/trino-product-tests-launcher/src/main/resources/docker/presto-product-tests/conf/environment/multinode-all/mariadb.properties new file mode 100644 index 000000000000..78530164025d --- /dev/null +++ b/testing/trino-product-tests-launcher/src/main/resources/docker/presto-product-tests/conf/environment/multinode-all/mariadb.properties @@ -0,0 +1,4 @@ +connector.name=mariadb +connection-url=jdbc:mariadb://mariadb:23306/ +connection-user=test +connection-password=test diff --git a/testing/trino-product-tests-launcher/src/main/resources/docker/presto-product-tests/conf/environment/multinode-mariadb/mariadb.properties b/testing/trino-product-tests-launcher/src/main/resources/docker/presto-product-tests/conf/environment/multinode-mariadb/mariadb.properties new file mode 100644 index 000000000000..78530164025d --- /dev/null +++ b/testing/trino-product-tests-launcher/src/main/resources/docker/presto-product-tests/conf/environment/multinode-mariadb/mariadb.properties @@ -0,0 +1,4 @@ +connector.name=mariadb +connection-url=jdbc:mariadb://mariadb:23306/ +connection-user=test +connection-password=test diff --git a/testing/trino-product-tests-launcher/src/main/resources/docker/presto-product-tests/conf/environment/multinode-mariadb/tempto-configuration.yaml b/testing/trino-product-tests-launcher/src/main/resources/docker/presto-product-tests/conf/environment/multinode-mariadb/tempto-configuration.yaml new file mode 100644 index 000000000000..adda7664eb00 --- /dev/null +++ b/testing/trino-product-tests-launcher/src/main/resources/docker/presto-product-tests/conf/environment/multinode-mariadb/tempto-configuration.yaml @@ -0,0 +1,3 @@ +databases: + presto: + jdbc_url: "jdbc:trino://${databases.presto.host}:${databases.presto.port}/mariadb/test" diff --git a/testing/trino-product-tests/pom.xml b/testing/trino-product-tests/pom.xml index 27c590466859..dfa34545577c 100644 --- a/testing/trino-product-tests/pom.xml +++ b/testing/trino-product-tests/pom.xml @@ -213,6 +213,12 @@ runtime + + org.mariadb.jdbc + mariadb-java-client + runtime + + org.postgresql postgresql diff --git a/testing/trino-product-tests/src/main/java/io/trino/tests/product/TestGroups.java b/testing/trino-product-tests/src/main/java/io/trino/tests/product/TestGroups.java index 85c689e759bf..389336d1df5d 100644 --- a/testing/trino-product-tests/src/main/java/io/trino/tests/product/TestGroups.java +++ b/testing/trino-product-tests/src/main/java/io/trino/tests/product/TestGroups.java @@ -70,6 +70,7 @@ public final class TestGroups public static final String PHOENIX = "phoenix"; public static final String CLICKHOUSE = "clickhouse"; public static final String KUDU = "kudu"; + public static final String MARIADB = "mariadb"; private TestGroups() {} } diff --git a/testing/trino-product-tests/src/main/java/io/trino/tests/product/mariadb/TestMariaDb.java b/testing/trino-product-tests/src/main/java/io/trino/tests/product/mariadb/TestMariaDb.java new file mode 100644 index 000000000000..c8b32f316af7 --- /dev/null +++ b/testing/trino-product-tests/src/main/java/io/trino/tests/product/mariadb/TestMariaDb.java @@ -0,0 +1,43 @@ +/* + * 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.tests.product.mariadb; + +import io.trino.tempto.ProductTest; +import io.trino.tempto.query.QueryResult; +import org.testng.annotations.Test; + +import static io.trino.tempto.assertions.QueryAssert.Row.row; +import static io.trino.tempto.assertions.QueryAssert.assertThat; +import static io.trino.tests.product.TestGroups.MARIADB; +import static io.trino.tests.product.TestGroups.PROFILE_SPECIFIC_TESTS; +import static io.trino.tests.product.utils.QueryExecutors.onTrino; + +public class TestMariaDb + extends ProductTest +{ + @Test(groups = {MARIADB, PROFILE_SPECIFIC_TESTS}) + public void testCreateTableAsSelect() + { + onTrino().executeQuery("DROP TABLE IF EXISTS mariadb.test.nation"); + QueryResult result = onTrino().executeQuery("CREATE TABLE mariadb.test.nation AS SELECT * FROM tpch.tiny.nation"); + try { + assertThat(result).updatedRowsCountIsEqualTo(25); + assertThat(onTrino().executeQuery("SELECT COUNT(*) FROM mariadb.test.nation")) + .containsOnly(row(25)); + } + finally { + onTrino().executeQuery("DROP TABLE mariadb.test.nation"); + } + } +} diff --git a/testing/trino-server-dev/etc/catalog/mariadb.properties b/testing/trino-server-dev/etc/catalog/mariadb.properties new file mode 100644 index 000000000000..72f0e29b46b4 --- /dev/null +++ b/testing/trino-server-dev/etc/catalog/mariadb.properties @@ -0,0 +1,4 @@ +connector.name=mariadb +connection-url=jdbc:mariadb://mariadb:23306 +connection-user=test +connection-password=test diff --git a/testing/trino-server-dev/etc/config.properties b/testing/trino-server-dev/etc/config.properties index 657f74fe9c95..dbd46a7374b1 100644 --- a/testing/trino-server-dev/etc/config.properties +++ b/testing/trino-server-dev/etc/config.properties @@ -42,6 +42,7 @@ plugin.bundles=\ ../../plugin/trino-tpch/pom.xml, \ ../../plugin/trino-local-file/pom.xml, \ ../../plugin/trino-mysql/pom.xml,\ + ../../plugin/trino-mariadb/pom.xml,\ ../../plugin/trino-singlestore/pom.xml,\ ../../plugin/trino-sqlserver/pom.xml, \ ../../plugin/trino-prometheus/pom.xml, \