Skip to content

Commit 198b20c

Browse files
committed
Add support create table with auto increment column in MySQL
1 parent e65dbdd commit 198b20c

File tree

7 files changed

+321
-9
lines changed

7 files changed

+321
-9
lines changed

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,7 @@ following features:
376376
- [](/sql/drop-table)
377377
- [](/sql/create-schema)
378378
- [](/sql/drop-schema)
379+
- [](mysql-schema-and-table-management)
379380
- [](mysql-procedures)
380381
- [](mysql-table-functions)
381382

@@ -395,6 +396,41 @@ following features:
395396
```{include} non-transactional-merge.fragment
396397
```
397398

399+
(mysql-schema-and-table-management)=
400+
### Schema and table management
401+
402+
#### Column properties
403+
404+
Column property usage example:
405+
406+
```
407+
CREATE TABLE person (
408+
id INT NOT NULL WITH (auto_increment = true),
409+
name VARCHAR,
410+
age INT,
411+
birthday DATE
412+
)
413+
WITH (
414+
primary_key = ARRAY['id']
415+
);
416+
```
417+
418+
The following are supported MySQL column properties:
419+
420+
:::{list-table}
421+
:widths: 30, 10, 60
422+
:header-rows: 1
423+
424+
* - Property name
425+
- Required
426+
- Description
427+
* - `auto_increment`
428+
- No
429+
- Auto generate a unique identity for new rows. There can be only one auto increment column
430+
and must be defined as the first key. Only applies to integer types (`TINYINT`,
431+
`SMALLINT`, `INTEGER`, `BIGINT`) column.
432+
:::
433+
398434
(mysql-procedures)=
399435
### Procedures
400436

plugin/trino-base-jdbc/src/main/java/io/trino/plugin/jdbc/BaseJdbcClient.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,13 +336,16 @@ public List<JdbcColumnHandle> getColumns(ConnectorSession session, SchemaTableNa
336336
boolean nullable = (resultSet.getInt("NULLABLE") != columnNoNulls);
337337
// Note: some databases (e.g. SQL Server) do not return column remarks/comment here.
338338
Optional<String> comment = Optional.ofNullable(emptyToNull(resultSet.getString("REMARKS")));
339+
// Note: the default implementation returns an empty map, and each connector needs to provide its own implementation.
340+
Map<String, Object> columnProperties = getColumnProperties(resultSet);
339341
// skip unsupported column types
340342
columnMapping.ifPresent(mapping -> columns.add(JdbcColumnHandle.builder()
341343
.setColumnName(columnName)
342344
.setJdbcTypeHandle(typeHandle)
343345
.setColumnType(mapping.getType())
344346
.setNullable(nullable)
345347
.setComment(comment)
348+
.setColumnProperties(columnProperties)
346349
.build()));
347350
if (columnMapping.isEmpty()) {
348351
UnsupportedTypeHandling unsupportedTypeHandling = getUnsupportedTypeHandling(session);
@@ -366,6 +369,12 @@ public List<JdbcColumnHandle> getColumns(ConnectorSession session, SchemaTableNa
366369
}
367370
}
368371

372+
public Map<String, Object> getColumnProperties(ResultSet resultSet)
373+
throws SQLException
374+
{
375+
return emptyMap();
376+
}
377+
369378
@Override
370379
public Iterator<RelationColumnsMetadata> getAllTableColumns(ConnectorSession session, Optional<String> schema)
371380
{

plugin/trino-base-jdbc/src/main/java/io/trino/plugin/jdbc/JdbcColumnHandle.java

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,24 @@
1616
import com.fasterxml.jackson.annotation.JsonCreator;
1717
import com.fasterxml.jackson.annotation.JsonProperty;
1818
import com.google.common.base.Joiner;
19+
import com.google.common.collect.ImmutableMap;
1920
import io.airlift.slice.SizeOf;
21+
import io.trino.spi.TrinoException;
2022
import io.trino.spi.connector.ColumnHandle;
2123
import io.trino.spi.connector.ColumnMetadata;
2224
import io.trino.spi.connector.ColumnSchema;
2325
import io.trino.spi.type.Type;
2426

27+
import java.util.List;
28+
import java.util.Map;
2529
import java.util.Objects;
2630
import java.util.Optional;
2731

2832
import static io.airlift.slice.SizeOf.estimatedSizeOf;
2933
import static io.airlift.slice.SizeOf.instanceSize;
3034
import static io.airlift.slice.SizeOf.sizeOf;
35+
import static io.trino.spi.StandardErrorCode.INVALID_COLUMN_PROPERTY;
36+
import static java.util.Collections.emptyMap;
3137
import static java.util.Objects.requireNonNull;
3238

3339
public final class JdbcColumnHandle
@@ -40,11 +46,12 @@ public final class JdbcColumnHandle
4046
private final Type columnType;
4147
private final boolean nullable;
4248
private final Optional<String> comment;
49+
private final Map<String, Object> columnProperties;
4350

4451
// All and only required fields
4552
public JdbcColumnHandle(String columnName, JdbcTypeHandle jdbcTypeHandle, Type columnType)
4653
{
47-
this(columnName, jdbcTypeHandle, columnType, true, Optional.empty());
54+
this(columnName, jdbcTypeHandle, columnType, true, Optional.empty(), emptyMap());
4855
}
4956

5057
/**
@@ -57,13 +64,15 @@ public JdbcColumnHandle(
5764
@JsonProperty("jdbcTypeHandle") JdbcTypeHandle jdbcTypeHandle,
5865
@JsonProperty("columnType") Type columnType,
5966
@JsonProperty("nullable") boolean nullable,
60-
@JsonProperty("comment") Optional<String> comment)
67+
@JsonProperty("comment") Optional<String> comment,
68+
@JsonProperty("columnProperties") Map<String, Object> columnProperties)
6169
{
6270
this.columnName = requireNonNull(columnName, "columnName is null");
6371
this.jdbcTypeHandle = requireNonNull(jdbcTypeHandle, "jdbcTypeHandle is null");
6472
this.columnType = requireNonNull(columnType, "columnType is null");
6573
this.nullable = nullable;
6674
this.comment = requireNonNull(comment, "comment is null");
75+
this.columnProperties = ImmutableMap.copyOf(columnProperties);
6776
}
6877

6978
@JsonProperty
@@ -96,13 +105,20 @@ public Optional<String> getComment()
96105
return comment;
97106
}
98107

108+
@JsonProperty
109+
public Map<String, Object> getColumnProperties()
110+
{
111+
return columnProperties;
112+
}
113+
99114
public ColumnMetadata getColumnMetadata()
100115
{
101116
return ColumnMetadata.builder()
102117
.setName(columnName)
103118
.setType(columnType)
104119
.setNullable(nullable)
105120
.setComment(comment)
121+
.setProperties(columnProperties)
106122
.build();
107123
}
108124

@@ -149,7 +165,25 @@ public long getRetainedSizeInBytes()
149165
+ sizeOf(nullable)
150166
+ estimatedSizeOf(columnName)
151167
+ sizeOf(comment, SizeOf::estimatedSizeOf)
152-
+ jdbcTypeHandle.getRetainedSizeInBytes();
168+
+ jdbcTypeHandle.getRetainedSizeInBytes()
169+
+ estimatedSizeOf(columnProperties, SizeOf::estimatedSizeOf, JdbcColumnHandle::estimatedObjectSizeOf);
170+
}
171+
172+
/**
173+
* Returns the estimated size of a property value.
174+
* <p>
175+
* Supported types are the same as those accepted by
176+
* {@link io.trino.metadata.PropertyUtil#toExpression}.
177+
*/
178+
private static long estimatedObjectSizeOf(Object value)
179+
{
180+
return switch (value) {
181+
case String stringValue -> estimatedSizeOf(stringValue);
182+
case Boolean _, Integer _, Long _, Double _ -> instanceSize(value.getClass());
183+
case List<?> list -> estimatedSizeOf(list, JdbcColumnHandle::estimatedObjectSizeOf);
184+
case null -> throw new TrinoException(INVALID_COLUMN_PROPERTY, "Property value is null");
185+
default -> throw new TrinoException(INVALID_COLUMN_PROPERTY, "Unsupported property value type: " + value.getClass().getName());
186+
};
153187
}
154188

155189
public static Builder builder()
@@ -169,6 +203,7 @@ public static final class Builder
169203
private Type columnType;
170204
private boolean nullable = true;
171205
private Optional<String> comment = Optional.empty();
206+
private Map<String, Object> columnProperties = emptyMap();
172207

173208
public Builder() {}
174209

@@ -179,6 +214,7 @@ private Builder(JdbcColumnHandle handle)
179214
this.columnType = handle.getColumnType();
180215
this.nullable = handle.isNullable();
181216
this.comment = handle.getComment();
217+
this.columnProperties = handle.getColumnProperties();
182218
}
183219

184220
public Builder setColumnName(String columnName)
@@ -211,14 +247,21 @@ public Builder setComment(Optional<String> comment)
211247
return this;
212248
}
213249

250+
public Builder setColumnProperties(Map<String, Object> columnProperties)
251+
{
252+
this.columnProperties = columnProperties;
253+
return this;
254+
}
255+
214256
public JdbcColumnHandle build()
215257
{
216258
return new JdbcColumnHandle(
217259
columnName,
218260
jdbcTypeHandle,
219261
columnType,
220262
nullable,
221-
comment);
263+
comment,
264+
columnProperties);
222265
}
223266
}
224267
}

plugin/trino-mysql/src/main/java/io/trino/plugin/mysql/MySqlClient.java

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,14 +83,18 @@
8383
import io.trino.spi.statistics.ColumnStatistics;
8484
import io.trino.spi.statistics.Estimate;
8585
import io.trino.spi.statistics.TableStatistics;
86+
import io.trino.spi.type.BigintType;
8687
import io.trino.spi.type.CharType;
8788
import io.trino.spi.type.DecimalType;
8889
import io.trino.spi.type.Decimals;
90+
import io.trino.spi.type.IntegerType;
8991
import io.trino.spi.type.LongTimestampWithTimeZone;
92+
import io.trino.spi.type.SmallintType;
9093
import io.trino.spi.type.StandardTypes;
9194
import io.trino.spi.type.TimeType;
9295
import io.trino.spi.type.TimestampType;
9396
import io.trino.spi.type.TimestampWithTimeZoneType;
97+
import io.trino.spi.type.TinyintType;
9498
import io.trino.spi.type.Type;
9599
import io.trino.spi.type.TypeManager;
96100
import io.trino.spi.type.TypeSignature;
@@ -182,6 +186,7 @@
182186
import static io.trino.plugin.jdbc.StandardColumnMappings.varcharWriteFunction;
183187
import static io.trino.plugin.jdbc.TypeHandlingJdbcSessionProperties.getUnsupportedTypeHandling;
184188
import static io.trino.plugin.jdbc.UnsupportedTypeHandling.CONVERT_TO_VARCHAR;
189+
import static io.trino.plugin.mysql.MySqlColumnProperties.AUTO_INCREMENT;
185190
import static io.trino.plugin.mysql.MySqlTableProperties.PRIMARY_KEY_PROPERTY;
186191
import static io.trino.spi.StandardErrorCode.ALREADY_EXISTS;
187192
import static io.trino.spi.StandardErrorCode.INVALID_TABLE_PROPERTY;
@@ -487,15 +492,30 @@ protected List<String> createTableSqls(RemoteTableName remoteTableName, List<Str
487492
columnDefinitions.addAll(columns);
488493

489494
List<String> primaryKeys = MySqlTableProperties.getPrimaryKey(tableMetadata.getProperties());
495+
verifyColumnMetadata(primaryKeys, tableMetadata.getColumns());
490496
if (!primaryKeys.isEmpty()) {
491-
verifyPrimaryKey(primaryKeys, tableMetadata.getColumns());
492497
columnDefinitions.add("PRIMARY KEY (" + primaryKeys.stream().map(this::quoted).collect(joining(", ")) + ")");
493498
}
494499
return ImmutableList.of(format("CREATE TABLE %s (%s) COMMENT %s", quoted(remoteTableName), join(", ", columnDefinitions.build()), mysqlVarcharLiteral(tableMetadata.getComment().orElse(NO_COMMENT))));
495500
}
496501

497-
private static void verifyPrimaryKey(List<String> primaryKeys, List<ColumnMetadata> columns)
502+
private static void verifyColumnMetadata(List<String> primaryKeys, List<ColumnMetadata> columns)
498503
{
504+
Set<String> autoIncrementColumnNames = columns.stream()
505+
.filter(column -> (boolean) column.getProperties().getOrDefault(AUTO_INCREMENT, false))
506+
.map(ColumnMetadata::getName)
507+
.collect(toImmutableSet());
508+
509+
if (autoIncrementColumnNames.size() > 1) {
510+
throw new TrinoException(NOT_SUPPORTED, "There can be only one auto increment column in MySQL");
511+
}
512+
513+
if (autoIncrementColumnNames.size() == 1) {
514+
if (primaryKeys.isEmpty() || !autoIncrementColumnNames.contains(primaryKeys.getFirst())) {
515+
throw new TrinoException(NOT_SUPPORTED, "Auto increment column must be defined as the first key in MySQL");
516+
}
517+
}
518+
499519
Set<String> columnNames = columns.stream()
500520
.map(column -> {
501521
String columnName = column.getName();
@@ -521,10 +541,23 @@ protected String getColumnDefinitionSql(ConnectorSession session, ColumnMetadata
521541
throw new TrinoException(NOT_SUPPORTED, "This connector does not support creating tables with column comment");
522542
}
523543

524-
return "%s %s %s".formatted(
544+
return "%s %s %s %s".formatted(
525545
quoted(columnName),
526546
toWriteMapping(session, column.getType()).getDataType(),
527-
column.isNullable() ? "NULL" : "NOT NULL");
547+
column.isNullable() ? "NULL" : "NOT NULL",
548+
isAutoIncrement(column) ? "AUTO_INCREMENT" : "");
549+
}
550+
551+
private static boolean isAutoIncrement(ColumnMetadata column)
552+
{
553+
boolean isAutoIncrement = (boolean) column.getProperties().getOrDefault(AUTO_INCREMENT, false);
554+
if (isAutoIncrement) {
555+
Type type = column.getType();
556+
if (!(type instanceof TinyintType || type instanceof SmallintType || type instanceof IntegerType || type instanceof BigintType)) {
557+
throw new TrinoException(NOT_SUPPORTED, "Unsupported column type for AUTO_INCREMENT: " + type);
558+
}
559+
}
560+
return isAutoIncrement;
528561
}
529562

530563
private static String mysqlVarcharLiteral(String value)
@@ -644,6 +677,18 @@ public Optional<ColumnMapping> toColumnMapping(ConnectorSession session, Connect
644677
return Optional.empty();
645678
}
646679

680+
@Override
681+
public Map<String, Object> getColumnProperties(ResultSet resultSet)
682+
throws SQLException
683+
{
684+
ImmutableMap.Builder<String, Object> columnPropertiesBuilder = ImmutableMap.builder();
685+
boolean autoIncrement = "YES".equals(resultSet.getString("IS_AUTOINCREMENT"));
686+
if (autoIncrement) {
687+
columnPropertiesBuilder.put(AUTO_INCREMENT, autoIncrement);
688+
}
689+
return columnPropertiesBuilder.buildOrThrow();
690+
}
691+
647692
private static ColumnMapping mySqlDefaultVarcharColumnMapping(int columnSize, Optional<CaseSensitivity> caseSensitivity)
648693
{
649694
if (columnSize > VarcharType.MAX_LENGTH) {
@@ -963,6 +1008,10 @@ private void addColumn(ConnectorSession session, RemoteTableName table, ColumnMe
9631008
throw new TrinoException(NOT_SUPPORTED, "This connector does not support adding columns with comments");
9641009
}
9651010

1011+
if (!column.getProperties().isEmpty()) {
1012+
throw new TrinoException(NOT_SUPPORTED, "This connector does not support adding columns with column properties");
1013+
}
1014+
9661015
try (Connection connection = connectionFactory.openConnection(session)) {
9671016
verify(connection.getAutoCommit());
9681017
String columnName = column.getName();

plugin/trino-mysql/src/main/java/io/trino/plugin/mysql/MySqlClientModule.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import static com.google.inject.multibindings.Multibinder.newSetBinder;
4141
import static com.google.inject.multibindings.OptionalBinder.newOptionalBinder;
4242
import static io.airlift.configuration.ConfigBinder.configBinder;
43+
import static io.trino.plugin.jdbc.JdbcModule.bindColumnPropertiesProvider;
4344
import static io.trino.plugin.jdbc.JdbcModule.bindTablePropertiesProvider;
4445

4546
public class MySqlClientModule
@@ -55,6 +56,7 @@ protected void setup(Binder binder)
5556
configBinder(binder).bindConfig(MySqlConfig.class);
5657
configBinder(binder).bindConfig(JdbcStatisticsConfig.class);
5758
bindTablePropertiesProvider(binder, MySqlTableProperties.class);
59+
bindColumnPropertiesProvider(binder, MySqlColumnProperties.class);
5860
install(new DecimalModule());
5961
install(new JdbcJoinPushdownSupportModule());
6062
newSetBinder(binder, ConnectorTableFunction.class).addBinding().toProvider(Query.class).in(Scopes.SINGLETON);

0 commit comments

Comments
 (0)