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, \