Skip to content

Commit 53de628

Browse files
#134 MySql table charset (#141)
Co-authored-by: Sebastian Bär <[email protected]>
1 parent 6985f32 commit 53de628

File tree

10 files changed

+251
-29
lines changed

10 files changed

+251
-29
lines changed

doc/changes/changes_3.6.0.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Code name: Fix CVE-2024-7254 in test dependency `com.google.protobuf:protobuf-ja
66

77
This release fixes CVE-2024-7254 in test dependency `com.google.protobuf:protobuf-java:3.25.1`.
88

9-
The release also speeds up inserting rows into a table by using batch insert.
9+
The release also speeds up inserting rows into a table by using batch insert and allows specifying a charset when creating MySQL tables, see the [user guide](../user_guide/user_guide.md#mysql-specific-database-objects) for details.
1010

1111
## Security
1212

@@ -15,6 +15,7 @@ The release also speeds up inserting rows into a table by using batch insert.
1515
## Features
1616

1717
* #137: Updated `AbstractImmediateDatabaseObjectWriter#write()` to use batching for inserting rows
18+
* #134: Allowed specifying charset for MySQL tables
1819

1920
## Dependency Updates
2021

doc/user_guide/user_guide.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ final Table table = schema.createTable("DAYS","DAY_NAME","VARCHAR(9), "SHORT_NAM
6262
In case you want to create more complex tables, you can also use a builder.
6363
6464
```java
65-
final Table table=schema.createTableBuilder("DAYS")
65+
final Table table = schema.createTableBuilder("DAYS")
6666
.column("DAY_NAME","VARCHAR(9)"
6767
.column("SHORT_NAME","VARCHAR(3)"
6868
.column("DAY_IN_WEEK","DECIMAL(1,0)"
@@ -390,6 +390,17 @@ Given that a script of that name exists, you can then [execute the script](#exec
390390

391391
## MySQL-Specific Database Objects
392392

393-
So far there are no MySQL Specific Database Objects that are not described in [Dialect-Agnostic Database Objects](#dialect-agnostic-database-objects) section.
393+
In addition to [Dialect-Agnostic Database Objects](#dialect-agnostic-database-objects), MySQL allows specifying a charset when creating a new table using the table builder of a `MySqlSchema`. When no charset is specified, MySql uses UTF8 as default.
394+
395+
```java
396+
final MySqlSchema schema = (MySqlSchema) factory.createSchema("TEST"));
397+
final MySqlTable table = schema.createTableBuilder("ASCII_DAYS")
398+
.charset("ASCII")
399+
.column("DAY_NAME","VARCHAR(9)"
400+
.column("SHORT_NAME","VARCHAR(3)"
401+
.column("DAY_IN_WEEK","DECIMAL(1,0)"
402+
// ...
403+
.build()
404+
```
394405

395406
Please keep in mind that Schema object represents a database in MySQL as a schema is a [synonym](https://dev.mysql.com/doc/refman/8.0/en/create-database.html) for a database in MySQL syntax.

src/main/java/com/exasol/dbbuilder/dialects/AbstractSchema.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public Table.Builder createTableBuilder(final String name) {
6363
public Table createTable(final String name, final List<String> columnNames, final List<String> columnTypes) {
6464
verifyNotDeleted();
6565
if (columnNames.size() == columnTypes.size()) {
66-
final Table.Builder builder = Table.builder(getWriter(), this, getIdentifier(name));
66+
final Table.Builder builder = createTableBuilder(name);
6767
passColumnsToTableBuilder(columnNames, columnTypes, builder);
6868
final Table table = builder.build();
6969
this.tables.add(table);

src/main/java/com/exasol/dbbuilder/dialects/Table.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ protected Table(final Builder builder) {
3030
* @param writer database object writer
3131
* @param schema parent schema
3232
* @param tableName name of the database table
33-
* @return new {@link Table} instance
33+
* @return new {@link Builder} instance
3434
*/
3535
// [impl->dsn~creating-tables~1]
3636
public static Builder builder(final DatabaseObjectWriter writer, final Schema schema, final Identifier tableName) {
@@ -151,4 +151,4 @@ public Table build() {
151151
return table;
152152
}
153153
}
154-
}
154+
}

src/main/java/com/exasol/dbbuilder/dialects/mysql/MySqlImmediateDatabaseObjectWriter.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,31 @@ public void write(final User user, final DatabaseObject object, final ObjectPriv
4646
}
4747
}
4848

49+
@Override
50+
public void write(final Table table) {
51+
final MySqlTable mySqlTable = (MySqlTable) table;
52+
final StringBuilder builder = new StringBuilder("CREATE TABLE ");
53+
builder.append(mySqlTable.getFullyQualifiedName()).append(" (");
54+
int i = 0;
55+
for (final Column column : mySqlTable.getColumns()) {
56+
if (i++ > 0) {
57+
builder.append(", ");
58+
}
59+
builder.append(getQuotedColumnName(column.getName())) //
60+
.append(" ") //
61+
.append(column.getType());
62+
}
63+
builder.append(")");
64+
if (mySqlTable.getCharset() != null) {
65+
builder.append(" CHARACTER SET ") //
66+
.append(mySqlTable.getCharset());
67+
}
68+
writeToObject(mySqlTable, builder.toString());
69+
}
70+
4971
@Override
5072
// [impl->dsn~dropping-schemas~2]
5173
public void drop(final Schema schema) {
5274
writeToObject(schema, "DROP SCHEMA " + schema.getFullyQualifiedName());
5375
}
54-
}
76+
}

src/main/java/com/exasol/dbbuilder/dialects/mysql/MySqlSchema.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,9 @@ public DatabaseObjectWriter getWriter() {
3131
protected Identifier getIdentifier(final String name) {
3232
return MySQLIdentifier.of(name);
3333
}
34+
35+
@Override
36+
public MySqlTable.Builder createTableBuilder(final String name) {
37+
return MySqlTable.builder(getWriter(), this, getIdentifier(name));
38+
}
3439
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package com.exasol.dbbuilder.dialects.mysql;
2+
3+
import com.exasol.db.Identifier;
4+
import com.exasol.dbbuilder.dialects.*;
5+
6+
/**
7+
* A MySql table that allows specifying a character set.
8+
*/
9+
public class MySqlTable extends Table {
10+
private final String charset;
11+
12+
/**
13+
* Create a new MySql table based on a given builder.
14+
*
15+
* @param builder builder from which to copy the values
16+
*/
17+
protected MySqlTable(final Builder builder) {
18+
super(builder);
19+
this.charset = builder.charset;
20+
}
21+
22+
/**
23+
* Get the table's character set.
24+
*
25+
* @return charset or {@code null} for the default charset
26+
*/
27+
public String getCharset() {
28+
return charset;
29+
}
30+
31+
/**
32+
* Create a builder for a {@link MySqlTable}.
33+
*
34+
* @param writer database object writer
35+
* @param parentSchema parent schema
36+
* @param tableName name of the database table
37+
* @return new {@link Builder} instance
38+
*/
39+
public static Builder builder(final DatabaseObjectWriter writer, final Schema parentSchema,
40+
final Identifier tableName) {
41+
return new Builder(writer, parentSchema, tableName);
42+
}
43+
44+
/**
45+
* Builder for {@link MySqlTable}s.
46+
*/
47+
public static class Builder extends Table.Builder {
48+
private String charset;
49+
50+
private Builder(final DatabaseObjectWriter writer, final Schema parentSchema, final Identifier tableName) {
51+
super(writer, parentSchema, tableName);
52+
}
53+
54+
@Override
55+
// Overriding this so that returned builder has the right type and users don't need to cast.
56+
public Builder column(final String columnName, final String columnType) {
57+
return (Builder) super.column(columnName, columnType);
58+
}
59+
60+
/**
61+
* Set a custom character set for the new table. Defaults to UTF-8.
62+
* <p>
63+
* This character set is then used for the whole table down to the columns. Additionally the standard collation
64+
* rules for this dataset are applied.
65+
* </p>
66+
*
67+
* @param charset custom charset, e.g. {@code ascii}
68+
* @return {@code this} for fluent programming
69+
*/
70+
public Builder charset(final String charset) {
71+
this.charset = charset;
72+
return this;
73+
}
74+
75+
/**
76+
* Build a new {@link MySqlTable} instance.
77+
*
78+
* @return new {@link MySqlTable} instance
79+
*/
80+
@Override
81+
public MySqlTable build() {
82+
final MySqlTable table = new MySqlTable(this);
83+
this.writer.write(table);
84+
return table;
85+
}
86+
}
87+
}

src/test/java/com/exasol/dbbuilder/dialects/AbstractObjectFactoryTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77

88
public abstract class AbstractObjectFactoryTest {
99

10-
abstract protected AbstractImmediateDatabaseObjectWriter getWriterMock();
10+
protected abstract AbstractImmediateDatabaseObjectWriter getWriterMock();
1111

12-
abstract protected DatabaseObjectFactory testee();
12+
protected abstract DatabaseObjectFactory testee();
1313

1414
@Test
1515
void createSchemaWritesObject() {

src/test/java/com/exasol/dbbuilder/dialects/mysql/MySQLDatabaseObjectCreationAndDeletionIT.java

Lines changed: 83 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@
99
import static org.hamcrest.Matchers.containsString;
1010
import static org.hamcrest.Matchers.equalTo;
1111
import static org.junit.jupiter.api.Assertions.assertAll;
12+
import static org.junit.jupiter.api.Assertions.assertTrue;
1213

1314
import java.sql.*;
1415

1516
import org.hamcrest.Matcher;
1617
import org.junit.jupiter.api.Tag;
1718
import org.junit.jupiter.api.Test;
19+
import org.testcontainers.containers.JdbcDatabaseContainer.NoDriverFoundException;
1820
import org.testcontainers.containers.MySQLContainer;
1921
import org.testcontainers.junit.jupiter.Container;
2022
import org.testcontainers.junit.jupiter.Testcontainers;
@@ -81,19 +83,21 @@ private void assertUserHasSchemaPrivilege(final String username, final String ob
8183

8284
@Test
8385
void testGrantSchemaPrivilegeToUser() {
84-
final Schema schema = this.factory.createSchema("OBJPRIVSCHEMA");
85-
final User user = this.factory.createUser("OBJPRIVUSER").grant(schema, SELECT, DELETE);
86-
assertAll(() -> assertUserHasSchemaPrivilege(user.getName(), schema.getName(), "Select_priv"),
87-
() -> assertUserHasSchemaPrivilege(user.getName(), schema.getName(), "Delete_priv"));
86+
try (final Schema schema = this.factory.createSchema("OBJPRIVSCHEMA")) {
87+
final User user = this.factory.createUser("OBJPRIVUSER").grant(schema, SELECT, DELETE);
88+
assertAll(() -> assertUserHasSchemaPrivilege(user.getName(), schema.getName(), "Select_priv"),
89+
() -> assertUserHasSchemaPrivilege(user.getName(), schema.getName(), "Delete_priv"));
90+
}
8891
}
8992

9093
@Test
9194
void testGrantTablePrivilegeToUser() {
92-
final Schema schema = this.factory.createSchema("TABPRIVSCHEMA");
93-
final Table table = schema.createTable("TABPRIVTABLE", "COL1", "DATE", "COL2", "INT");
94-
final User user = this.factory.createUser("TABPRIVUSER").grant(table, SELECT, DELETE);
95-
assertAll(() -> assertUserHasTablePrivilege(user.getName(), table.getName(), "Select"),
96-
() -> assertUserHasTablePrivilege(user.getName(), table.getName(), "Delete"));
95+
try (final Schema schema = this.factory.createSchema("TABPRIVSCHEMA")) {
96+
final Table table = schema.createTable("TABPRIVTABLE", "COL1", "DATE", "COL2", "INT");
97+
final User user = this.factory.createUser("TABPRIVUSER").grant(table, SELECT, DELETE);
98+
assertAll(() -> assertUserHasTablePrivilege(user.getName(), table.getName(), "Select"),
99+
() -> assertUserHasTablePrivilege(user.getName(), table.getName(), "Delete"));
100+
}
97101
}
98102

99103
private void assertUserHasTablePrivilege(final String username, final String objectName,
@@ -114,17 +118,76 @@ private void assertUserHasTablePrivilege(final String username, final String obj
114118

115119
@Test
116120
void testInsertIntoTable() {
117-
final Schema schema = this.factory.createSchema("INSERTSCHEMA");
118-
final Table table = schema.createTable("INSERTTABLE", "ID", "INT", "NAME", "VARCHAR(10)");
119-
table.insert(1, "FOO").insert(2, "BAR");
120-
try {
121-
final ResultSet result = this.adminConnection.createStatement()
122-
.executeQuery("SELECT ID, NAME FROM " + table.getFullyQualifiedName() + "ORDER BY ID ASC");
123-
assertThat(result, table().row(1, "FOO").row(2, "BAR").matches());
124-
} catch (final SQLException exception) {
125-
throw new AssertionError(ExaError.messageBuilder("E-TDBJ-25")
126-
.message("Unable to validate contents of table {{table}}", table.getFullyQualifiedName())
127-
.toString(), exception);
121+
try (final Schema schema = this.factory.createSchema("INSERTSCHEMA")) {
122+
final Table table = schema.createTable("INSERTTABLE", "ID", "INT", "NAME", "VARCHAR(10)");
123+
table.insert(1, "FOO").insert(2, "BAR");
124+
try {
125+
final ResultSet result = this.adminConnection.createStatement()
126+
.executeQuery("SELECT ID, NAME FROM " + table.getFullyQualifiedName() + "ORDER BY ID ASC");
127+
assertThat(result, table().row(1, "FOO").row(2, "BAR").matches());
128+
} catch (final SQLException exception) {
129+
throw new AssertionError(ExaError.messageBuilder("E-TDBJ-25")
130+
.message("Unable to validate contents of table {{table}}", table.getFullyQualifiedName())
131+
.toString(), exception);
132+
}
133+
}
134+
}
135+
136+
@Test
137+
void testCreateTableWithDefaultCharsetUsesUtf8() {
138+
try (final MySqlSchema schema = (MySqlSchema) this.factory.createSchema("CHARSET_SCHEMA_DEFAULT")) {
139+
final MySqlTable table = schema.createTableBuilder("TABLE_WITH_CHARSET").column("ID", "INT")
140+
.column("NAME", "VARCHAR(10)").build();
141+
assertAll(
142+
() -> assertThat("column charset",
143+
getColumnCharset("def", schema.getName(), table.getName(), "NAME"), equalTo("utf8mb4")),
144+
() -> assertThat("table collation", getTableCollation("def", schema.getName(), table.getName()),
145+
equalTo("utf8mb4_0900_ai_ci")));
146+
}
147+
}
148+
149+
@Test
150+
void testCreateTableWithCharset() {
151+
try (final MySqlSchema schema = (MySqlSchema) this.factory.createSchema("CHARSET_SCHEMA_ASCII")) {
152+
final MySqlTable table = schema.createTableBuilder("TABLE_WITH_CHARSET").charset("ASCII")
153+
.column("ID", "INT").column("NAME", "VARCHAR(10)").build();
154+
assertAll(
155+
() -> assertThat("column charset",
156+
getColumnCharset("def", schema.getName(), table.getName(), "NAME"), equalTo("ascii")),
157+
() -> assertThat("table collation", getTableCollation("def", schema.getName(), table.getName()),
158+
equalTo("ascii_general_ci")));
159+
}
160+
}
161+
162+
private String getColumnCharset(final String catalog, final String schema, final String table,
163+
final String column) {
164+
final String query = "select CHARACTER_SET_NAME from information_schema.COLUMNS "
165+
+ "where TABLE_CATALOG=? AND TABLE_SCHEMA=? AND TABLE_NAME=? AND COLUMN_NAME=?";
166+
try (Connection con = container.createConnection(""); PreparedStatement stmt = con.prepareStatement(query)) {
167+
stmt.setString(1, catalog);
168+
stmt.setString(2, schema);
169+
stmt.setString(3, table);
170+
stmt.setString(4, column);
171+
final ResultSet rs = stmt.executeQuery();
172+
assertTrue(rs.next());
173+
return rs.getString("CHARACTER_SET_NAME");
174+
} catch (NoDriverFoundException | SQLException exception) {
175+
throw new IllegalStateException("Query '" + query + "' failed: " + exception.getMessage(), exception);
176+
}
177+
}
178+
179+
private String getTableCollation(final String catalog, final String schema, final String table) {
180+
final String query = "select TABLE_COLLATION from information_schema.TABLES "
181+
+ "where TABLE_CATALOG=? AND TABLE_SCHEMA=? AND TABLE_NAME=?";
182+
try (Connection con = container.createConnection(""); PreparedStatement stmt = con.prepareStatement(query)) {
183+
stmt.setString(1, catalog);
184+
stmt.setString(2, schema);
185+
stmt.setString(3, table);
186+
final ResultSet rs = stmt.executeQuery();
187+
assertTrue(rs.next());
188+
return rs.getString("TABLE_COLLATION");
189+
} catch (NoDriverFoundException | SQLException exception) {
190+
throw new IllegalStateException("Query '" + query + "' failed: " + exception.getMessage(), exception);
128191
}
129192
}
130193

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.exasol.dbbuilder.dialects.mysql;
2+
3+
import static org.hamcrest.MatcherAssert.assertThat;
4+
import static org.hamcrest.Matchers.*;
5+
6+
import org.junit.jupiter.api.Test;
7+
import org.junit.jupiter.api.extension.ExtendWith;
8+
import org.mockito.Mock;
9+
import org.mockito.junit.jupiter.MockitoExtension;
10+
11+
import com.exasol.dbbuilder.dialects.DatabaseObjectWriter;
12+
import com.exasol.dbbuilder.dialects.Schema;
13+
14+
@ExtendWith(MockitoExtension.class)
15+
class MySqlTableTest {
16+
@Mock
17+
DatabaseObjectWriter writerMock;
18+
@Mock
19+
Schema schemaMock;
20+
21+
@Test
22+
void createWithoutCharset() {
23+
final MySqlTable table = MySqlTable.builder(writerMock, schemaMock, MySQLIdentifier.of("tableName")).build();
24+
assertThat(table.getCharset(), is(nullValue()));
25+
}
26+
27+
@Test
28+
void createWithCharset() {
29+
final MySqlTable table = MySqlTable.builder(writerMock, schemaMock, MySQLIdentifier.of("tableName"))
30+
.charset("myCharset").build();
31+
assertThat(table.getCharset(), equalTo("myCharset"));
32+
}
33+
}

0 commit comments

Comments
 (0)