From 50615628a2e24e8bcb577cfdf894dd729ea79a1e Mon Sep 17 00:00:00 2001 From: "kirk.hansen" <3529086+kirkhansen@users.noreply.github.com> Date: Tue, 3 Feb 2026 13:52:12 -0600 Subject: [PATCH] Add support for JSON type in SQLServer --- docs/src/main/sphinx/connector/sqlserver.md | 6 +++++ .../plugin/sqlserver/SqlServerClient.java | 24 +++++++++++++++++++ .../sqlserver/BaseSqlServerTypeMapping.java | 23 ++++++++++++++++++ .../plugin/sqlserver/TestSqlServerClient.java | 2 ++ .../sqlserver/TestSqlServerTypeMapping.java | 10 ++++++++ 5 files changed, 65 insertions(+) diff --git a/docs/src/main/sphinx/connector/sqlserver.md b/docs/src/main/sphinx/connector/sqlserver.md index 469864d9fe9e..c8ba6d888ce5 100644 --- a/docs/src/main/sphinx/connector/sqlserver.md +++ b/docs/src/main/sphinx/connector/sqlserver.md @@ -234,6 +234,9 @@ The connector maps SQL Server types to the corresponding Trino types following t * - `DATETIMEOFFSET[(n)]` - `TIMESTAMP(n) WITH TIME ZONE` - `0 <= n <= 7` +* - `JSON` + - `JSON` + - ::: ### Trino type to SQL Server type mapping @@ -289,6 +292,9 @@ The connector maps Trino types to the corresponding SQL Server types following t * - `TIMESTAMP(n)` - `DATETIME2(n)` - `0 <= n <= 7` +* - `JSON` + - `JSON` + - ::: Complete list of [SQL Server data types](https://msdn.microsoft.com/library/ms187752.aspx). diff --git a/plugin/trino-sqlserver/src/main/java/io/trino/plugin/sqlserver/SqlServerClient.java b/plugin/trino-sqlserver/src/main/java/io/trino/plugin/sqlserver/SqlServerClient.java index fe979ebbbec1..ab7ce4b5611c 100644 --- a/plugin/trino-sqlserver/src/main/java/io/trino/plugin/sqlserver/SqlServerClient.java +++ b/plugin/trino-sqlserver/src/main/java/io/trino/plugin/sqlserver/SqlServerClient.java @@ -97,6 +97,8 @@ import io.trino.spi.type.TimestampType; import io.trino.spi.type.TimestampWithTimeZoneType; import io.trino.spi.type.Type; +import io.trino.spi.type.TypeManager; +import io.trino.spi.type.TypeSignature; import io.trino.spi.type.VarbinaryType; import io.trino.spi.type.VarcharType; import microsoft.sql.DateTimeOffset; @@ -137,7 +139,9 @@ import static com.google.common.base.Throwables.throwIfInstanceOf; import static com.google.common.collect.MoreCollectors.toOptional; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; +import static io.airlift.slice.Slices.utf8Slice; import static io.airlift.slice.Slices.wrappedBuffer; +import static io.trino.plugin.base.util.JsonTypeUtil.jsonParse; import static io.trino.plugin.jdbc.CaseSensitivity.CASE_INSENSITIVE; import static io.trino.plugin.jdbc.CaseSensitivity.CASE_SENSITIVE; import static io.trino.plugin.jdbc.JdbcErrorCode.JDBC_ERROR; @@ -191,6 +195,7 @@ 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.StandardTypes.JSON; import static io.trino.spi.type.TimeType.createTimeType; import static io.trino.spi.type.TimeZoneKey.UTC_KEY; import static io.trino.spi.type.TimeZoneKey.getTimeZoneKey; @@ -236,6 +241,7 @@ public class SqlServerClient private final boolean statisticsEnabled; + private final Type jsonType; private final ConnectorExpressionRewriter connectorExpressionRewriter; private final AggregateFunctionRewriter aggregateFunctionRewriter; @@ -301,11 +307,14 @@ public SqlServerClient( JdbcStatisticsConfig statisticsConfig, ConnectionFactory connectionFactory, QueryBuilder queryBuilder, + TypeManager typeManager, IdentifierMapping identifierMapping, RemoteQueryModifier queryModifier) { super("\"", connectionFactory, queryBuilder, config.getJdbcTypesMappedToVarchar(), identifierMapping, queryModifier, true); + requireNonNull(typeManager, "typeManager is null"); + jsonType = typeManager.getType(new TypeSignature(JSON)); this.statisticsEnabled = statisticsConfig.isEnabled(); this.connectorExpressionRewriter = JdbcConnectorExpressionRewriterBuilder.newBuilder() @@ -601,6 +610,8 @@ public Optional toColumnMapping(ConnectorSession session, Connect return Optional.of(varbinaryColumnMapping()); case "datetimeoffset": return Optional.of(timestampWithTimeZoneColumnMapping(typeHandle.requiredDecimalDigits())); + case "json": + return Optional.of(jsonColumnMapping()); } switch (typeHandle.jdbcType()) { @@ -760,6 +771,10 @@ public WriteMapping toWriteMapping(ConnectorSession session, Type type) return WriteMapping.objectMapping(dataType, longTimestampWriteFunction(timestampType, precision)); } + if (type == jsonType) { + return WriteMapping.sliceMapping("json", nvarcharWriteFunction()); + } + throw new TrinoException(NOT_SUPPORTED, "Unsupported column type: " + type.getDisplayName()); } @@ -1329,6 +1344,15 @@ private static ColumnMapping varcharColumnMapping(int varcharLength, boolean isC isCaseSensitive ? SQLSERVER_CHARACTER_PUSHDOWN : CASE_INSENSITIVE_CHARACTER_PUSHDOWN); } + private ColumnMapping jsonColumnMapping() + { + return ColumnMapping.sliceMapping( + jsonType, + (resultSet, columnIndex) -> jsonParse(utf8Slice(resultSet.getString(columnIndex))), + nvarcharWriteFunction(), + DISABLE_PUSHDOWN); + } + private static SliceWriteFunction nvarcharWriteFunction() { return (statement, index, value) -> statement.setNString(index, value.toStringUtf8()); diff --git a/plugin/trino-sqlserver/src/test/java/io/trino/plugin/sqlserver/BaseSqlServerTypeMapping.java b/plugin/trino-sqlserver/src/test/java/io/trino/plugin/sqlserver/BaseSqlServerTypeMapping.java index 8b28bdaf2cf2..bd3e06f251cb 100644 --- a/plugin/trino-sqlserver/src/test/java/io/trino/plugin/sqlserver/BaseSqlServerTypeMapping.java +++ b/plugin/trino-sqlserver/src/test/java/io/trino/plugin/sqlserver/BaseSqlServerTypeMapping.java @@ -56,6 +56,7 @@ import static io.trino.spi.type.VarcharType.createVarcharType; import static io.trino.sql.planner.assertions.PlanMatchPattern.tableScan; import static io.trino.testing.TestingNames.randomNameSuffix; +import static io.trino.type.JsonType.JSON; import static java.lang.String.format; import static java.time.ZoneOffset.UTC; import static org.assertj.core.api.Assertions.assertThat; @@ -998,6 +999,28 @@ public void testSqlServerDatetimeOffsetHistoricalDatesRangeQuery() } } + @Test + public void testJson() + { + // Conforms to RFC 4627, so not scalar values, but only objects and arrays. + SqlDataTypeTest.create() + .addRoundTrip("json", "NULL", JSON, "CAST(NULL AS JSON)") + .addRoundTrip("json", "JSON '{}'", JSON, "JSON '{}'") + .addRoundTrip("json", "JSON '{\"a\":1,\"b\":2}'", JSON, "JSON '{\"a\":1,\"b\":2}'") + .addRoundTrip("json", "JSON '{\"a\":[1,2,3],\"b\":{\"aa\":11,\"bb\":[{\"a\":1,\"b\":2},{\"a\":0}]}}'", JSON, "JSON '{\"a\":[1,2,3],\"b\":{\"aa\":11,\"bb\":[{\"a\":1,\"b\":2},{\"a\":0}]}}'") + .addRoundTrip("json", "JSON '[]'", JSON, "JSON '[]'") + .execute(getQueryRunner(), sqlServerCreateAndTrinoInsert("test_json")); + + SqlDataTypeTest.create() + .addRoundTrip("json", "NULL", JSON, "CAST(NULL AS JSON)") + .addRoundTrip("json", "JSON '{}'", JSON, "JSON '{}'") + .addRoundTrip("json", "JSON '{\"a\":1,\"b\":2}'", JSON, "JSON '{\"a\":1,\"b\":2}'") + .addRoundTrip("json", "JSON '{\"a\":[1,2,3],\"b\":{\"aa\":11,\"bb\":[{\"a\":1,\"b\":2},{\"a\":0}]}}'", JSON, "JSON '{\"a\":[1,2,3],\"b\":{\"aa\":11,\"bb\":[{\"a\":1,\"b\":2},{\"a\":0}]}}'") + .addRoundTrip("json", "JSON '[]'", JSON, "JSON '[]'") + .execute(getQueryRunner(), trinoCreateAsSelect("test_json")) + .execute(getQueryRunner(), trinoCreateAndInsert("test_json")); + } + protected DataSetup trinoCreateAsSelect(String tableNamePrefix) { return trinoCreateAsSelect(getSession(), tableNamePrefix); diff --git a/plugin/trino-sqlserver/src/test/java/io/trino/plugin/sqlserver/TestSqlServerClient.java b/plugin/trino-sqlserver/src/test/java/io/trino/plugin/sqlserver/TestSqlServerClient.java index 50958eed81d9..f09dc32c7368 100644 --- a/plugin/trino-sqlserver/src/test/java/io/trino/plugin/sqlserver/TestSqlServerClient.java +++ b/plugin/trino-sqlserver/src/test/java/io/trino/plugin/sqlserver/TestSqlServerClient.java @@ -38,6 +38,7 @@ 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.type.InternalTypeManager.TESTING_TYPE_MANAGER; import static org.assertj.core.api.Assertions.assertThat; public class TestSqlServerClient @@ -63,6 +64,7 @@ public class TestSqlServerClient throw new UnsupportedOperationException(); }, new DefaultQueryBuilder(RemoteQueryModifier.NONE), + TESTING_TYPE_MANAGER, new DefaultIdentifierMapping(), RemoteQueryModifier.NONE); diff --git a/plugin/trino-sqlserver/src/test/java/io/trino/plugin/sqlserver/TestSqlServerTypeMapping.java b/plugin/trino-sqlserver/src/test/java/io/trino/plugin/sqlserver/TestSqlServerTypeMapping.java index 762ffdb5b94f..361feb5322bf 100644 --- a/plugin/trino-sqlserver/src/test/java/io/trino/plugin/sqlserver/TestSqlServerTypeMapping.java +++ b/plugin/trino-sqlserver/src/test/java/io/trino/plugin/sqlserver/TestSqlServerTypeMapping.java @@ -14,6 +14,9 @@ package io.trino.plugin.sqlserver; import io.trino.testing.QueryRunner; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assumptions.abort; public class TestSqlServerTypeMapping extends BaseSqlServerTypeMapping @@ -26,4 +29,11 @@ protected QueryRunner createQueryRunner() return SqlServerQueryRunner.builder(sqlServer) .build(); } + + @Test + @Override + public void testJson() + { + abort("json type is not supported in SQL Server 2019-CU28-ubuntu-20.04"); + } }