Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#134 MySql table charset #141

Merged
merged 13 commits into from
Sep 24, 2024
3 changes: 2 additions & 1 deletion doc/changes/changes_3.6.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Code name: Fix CVE-2024-7254 in test dependency `com.google.protobuf:protobuf-ja

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

The release also speeds up inserting rows into a table by using batch insert.
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.

## Security

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

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

## Dependency Updates

Expand Down
15 changes: 13 additions & 2 deletions doc/user_guide/user_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ final Table table = schema.createTable("DAYS","DAY_NAME","VARCHAR(9), "SHORT_NAM
In case you want to create more complex tables, you can also use a builder.

```java
final Table table=schema.createTableBuilder("DAYS")
final Table table = schema.createTableBuilder("DAYS")
.column("DAY_NAME","VARCHAR(9)"
.column("SHORT_NAME","VARCHAR(3)"
.column("DAY_IN_WEEK","DECIMAL(1,0)"
Expand Down Expand Up @@ -390,6 +390,17 @@ Given that a script of that name exists, you can then [execute the script](#exec

## MySQL-Specific Database Objects

So far there are no MySQL Specific Database Objects that are not described in [Dialect-Agnostic Database Objects](#dialect-agnostic-database-objects) section.
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.

```java
final MySqlSchema schema = (MySqlSchema) factory.createSchema("TEST"));
final MySqlTable table = schema.createTableBuilder("ASCII_DAYS")
.charset("ASCII")
.column("DAY_NAME","VARCHAR(9)"
.column("SHORT_NAME","VARCHAR(3)"
.column("DAY_IN_WEEK","DECIMAL(1,0)"
// ...
.build()
```

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.
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public Table.Builder createTableBuilder(final String name) {
public Table createTable(final String name, final List<String> columnNames, final List<String> columnTypes) {
verifyNotDeleted();
if (columnNames.size() == columnTypes.size()) {
final Table.Builder builder = Table.builder(getWriter(), this, getIdentifier(name));
final Table.Builder builder = createTableBuilder(name);
passColumnsToTableBuilder(columnNames, columnTypes, builder);
final Table table = builder.build();
this.tables.add(table);
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/com/exasol/dbbuilder/dialects/Table.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ protected Table(final Builder builder) {
* @param writer database object writer
* @param schema parent schema
* @param tableName name of the database table
* @return new {@link Table} instance
* @return new {@link Builder} instance
*/
// [impl->dsn~creating-tables~1]
public static Builder builder(final DatabaseObjectWriter writer, final Schema schema, final Identifier tableName) {
Expand Down Expand Up @@ -151,4 +151,4 @@ public Table build() {
return table;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,31 @@ public void write(final User user, final DatabaseObject object, final ObjectPriv
}
}

@Override
public void write(final Table table) {
final MySqlTable mySqlTable = (MySqlTable) table;
final StringBuilder builder = new StringBuilder("CREATE TABLE ");
builder.append(mySqlTable.getFullyQualifiedName()).append(" (");
int i = 0;
for (final Column column : mySqlTable.getColumns()) {
if (i++ > 0) {
builder.append(", ");
}
builder.append(getQuotedColumnName(column.getName())) //
.append(" ") //
.append(column.getType());
}
builder.append(")");
if (mySqlTable.getCharset() != null) {
builder.append(" CHARACTER SET ") //
.append(mySqlTable.getCharset());
}
writeToObject(mySqlTable, builder.toString());
}

@Override
// [impl->dsn~dropping-schemas~2]
public void drop(final Schema schema) {
writeToObject(schema, "DROP SCHEMA " + schema.getFullyQualifiedName());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,9 @@ public DatabaseObjectWriter getWriter() {
protected Identifier getIdentifier(final String name) {
return MySQLIdentifier.of(name);
}

@Override
public MySqlTable.Builder createTableBuilder(final String name) {
return MySqlTable.builder(getWriter(), this, getIdentifier(name));
}
}
85 changes: 85 additions & 0 deletions src/main/java/com/exasol/dbbuilder/dialects/mysql/MySqlTable.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package com.exasol.dbbuilder.dialects.mysql;

import com.exasol.db.Identifier;
import com.exasol.dbbuilder.dialects.*;

/**
* A MySql table that allows specifying a charset.
kaklakariada marked this conversation as resolved.
Show resolved Hide resolved
*/
public class MySqlTable extends Table {

private final String charset;
kaklakariada marked this conversation as resolved.
Show resolved Hide resolved

/**
* Create a new MySql table based on a given builder.
*
* @param builder the builder from which to copy the values
kaklakariada marked this conversation as resolved.
Show resolved Hide resolved
*/
protected MySqlTable(final Builder builder) {
super(builder);
this.charset = builder.charset;
}

/**
* Get the table's charset.
kaklakariada marked this conversation as resolved.
Show resolved Hide resolved
*
* @return charset or {@code null} for the default charset
*/
public String getCharset() {
return charset;
}

/**
* Create a builder for a {@link MySqlTable}.
*
* @param writer database object writer
* @param parentSchema parent schema
* @param tableName name of the database table
* @return new {@link Builder} instance
*/
public static Builder builder(final DatabaseObjectWriter writer, final Schema parentSchema,
final Identifier tableName) {
return new Builder(writer, parentSchema, tableName);
}

/**
* Builder for {@link MySqlTable}s.
*/
public static class Builder extends Table.Builder {

private String charset;
kaklakariada marked this conversation as resolved.
Show resolved Hide resolved

private Builder(final DatabaseObjectWriter writer, final Schema parentSchema, final Identifier tableName) {
super(writer, parentSchema, tableName);
}

@Override
kaklakariada marked this conversation as resolved.
Show resolved Hide resolved
public Builder column(final String columnName, final String columnType) {
super.column(columnName, columnType);
return this;
}

/**
* Set a custom charset for the new table. Defaults to UTF-8.
kaklakariada marked this conversation as resolved.
Show resolved Hide resolved
*
* @param charset custom charset, e.g. {@code ascii}
* @return {@code this} for fluent programming
*/
public Builder charset(final String charset) {
this.charset = charset;
return this;
}

/**
* Build a new {@link MySqlTable} instance.
*
* @return new {@link MySqlTable} instance
*/
@Override
public MySqlTable build() {
final MySqlTable table = new MySqlTable(this);
this.writer.write(table);
return table;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@

public abstract class AbstractObjectFactoryTest {

abstract protected AbstractImmediateDatabaseObjectWriter getWriterMock();
protected abstract AbstractImmediateDatabaseObjectWriter getWriterMock();

abstract protected DatabaseObjectFactory testee();
protected abstract DatabaseObjectFactory testee();

@Test
void createSchemaWritesObject() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.sql.*;

import org.hamcrest.Matcher;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.JdbcDatabaseContainer.NoDriverFoundException;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
Expand Down Expand Up @@ -81,19 +83,21 @@ private void assertUserHasSchemaPrivilege(final String username, final String ob

@Test
void testGrantSchemaPrivilegeToUser() {
final Schema schema = this.factory.createSchema("OBJPRIVSCHEMA");
final User user = this.factory.createUser("OBJPRIVUSER").grant(schema, SELECT, DELETE);
assertAll(() -> assertUserHasSchemaPrivilege(user.getName(), schema.getName(), "Select_priv"),
() -> assertUserHasSchemaPrivilege(user.getName(), schema.getName(), "Delete_priv"));
try (final Schema schema = this.factory.createSchema("OBJPRIVSCHEMA")) {
final User user = this.factory.createUser("OBJPRIVUSER").grant(schema, SELECT, DELETE);
assertAll(() -> assertUserHasSchemaPrivilege(user.getName(), schema.getName(), "Select_priv"),
() -> assertUserHasSchemaPrivilege(user.getName(), schema.getName(), "Delete_priv"));
}
}

@Test
void testGrantTablePrivilegeToUser() {
final Schema schema = this.factory.createSchema("TABPRIVSCHEMA");
final Table table = schema.createTable("TABPRIVTABLE", "COL1", "DATE", "COL2", "INT");
final User user = this.factory.createUser("TABPRIVUSER").grant(table, SELECT, DELETE);
assertAll(() -> assertUserHasTablePrivilege(user.getName(), table.getName(), "Select"),
() -> assertUserHasTablePrivilege(user.getName(), table.getName(), "Delete"));
try (final Schema schema = this.factory.createSchema("TABPRIVSCHEMA")) {
final Table table = schema.createTable("TABPRIVTABLE", "COL1", "DATE", "COL2", "INT");
final User user = this.factory.createUser("TABPRIVUSER").grant(table, SELECT, DELETE);
assertAll(() -> assertUserHasTablePrivilege(user.getName(), table.getName(), "Select"),
() -> assertUserHasTablePrivilege(user.getName(), table.getName(), "Delete"));
}
}

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

@Test
void testInsertIntoTable() {
final Schema schema = this.factory.createSchema("INSERTSCHEMA");
final Table table = schema.createTable("INSERTTABLE", "ID", "INT", "NAME", "VARCHAR(10)");
table.insert(1, "FOO").insert(2, "BAR");
try {
final ResultSet result = this.adminConnection.createStatement()
.executeQuery("SELECT ID, NAME FROM " + table.getFullyQualifiedName() + "ORDER BY ID ASC");
assertThat(result, table().row(1, "FOO").row(2, "BAR").matches());
} catch (final SQLException exception) {
throw new AssertionError(ExaError.messageBuilder("E-TDBJ-25")
.message("Unable to validate contents of table {{table}}", table.getFullyQualifiedName())
.toString(), exception);
try (final Schema schema = this.factory.createSchema("INSERTSCHEMA")) {
final Table table = schema.createTable("INSERTTABLE", "ID", "INT", "NAME", "VARCHAR(10)");
table.insert(1, "FOO").insert(2, "BAR");
try {
final ResultSet result = this.adminConnection.createStatement()
.executeQuery("SELECT ID, NAME FROM " + table.getFullyQualifiedName() + "ORDER BY ID ASC");
assertThat(result, table().row(1, "FOO").row(2, "BAR").matches());
} catch (final SQLException exception) {
throw new AssertionError(ExaError.messageBuilder("E-TDBJ-25")
.message("Unable to validate contents of table {{table}}", table.getFullyQualifiedName())
.toString(), exception);
}
}
}

@Test
void testCreateTableWithDefaultCharsetUsesUtf8() {
try (final MySqlSchema schema = (MySqlSchema) this.factory.createSchema("CHARSET_SCHEMA_DEFAULT")) {
final MySqlTable table = schema.createTableBuilder("TABLE_WITH_CHARSET").column("ID", "INT")
.column("NAME", "VARCHAR(10)").build();
assertAll(
() -> assertThat("column charset",
getColumnCharset("def", schema.getName(), table.getName(), "NAME"), equalTo("utf8mb4")),
() -> assertThat("table collation", getTableCollation("def", schema.getName(), table.getName()),
equalTo("utf8mb4_0900_ai_ci")));
}
}

@Test
void testCreateTableWithCharset() {
try (final MySqlSchema schema = (MySqlSchema) this.factory.createSchema("CHARSET_SCHEMA_ASCII")) {
final MySqlTable table = schema.createTableBuilder("TABLE_WITH_CHARSET").charset("ASCII")
.column("ID", "INT").column("NAME", "VARCHAR(10)").build();
assertAll(
() -> assertThat("column charset",
getColumnCharset("def", schema.getName(), table.getName(), "NAME"), equalTo("ascii")),
() -> assertThat("table collation", getTableCollation("def", schema.getName(), table.getName()),
equalTo("ascii_general_ci")));
}
}

private String getColumnCharset(final String catalog, final String schema, final String table,
final String column) {
final String query = "select CHARACTER_SET_NAME from information_schema.COLUMNS "
+ "where TABLE_CATALOG=? AND TABLE_SCHEMA=? AND TABLE_NAME=? AND COLUMN_NAME=?";
try (Connection con = container.createConnection(""); PreparedStatement stmt = con.prepareStatement(query)) {
stmt.setString(1, catalog);
stmt.setString(2, schema);
stmt.setString(3, table);
stmt.setString(4, column);
final ResultSet rs = stmt.executeQuery();
assertTrue(rs.next());
return rs.getString("CHARACTER_SET_NAME");
} catch (NoDriverFoundException | SQLException exception) {
throw new IllegalStateException("Query '" + query + "' failed: " + exception.getMessage(), exception);
}
}

private String getTableCollation(final String catalog, final String schema, final String table) {
final String query = "select TABLE_COLLATION from information_schema.TABLES "
+ "where TABLE_CATALOG=? AND TABLE_SCHEMA=? AND TABLE_NAME=?";
try (Connection con = container.createConnection(""); PreparedStatement stmt = con.prepareStatement(query)) {
stmt.setString(1, catalog);
stmt.setString(2, schema);
stmt.setString(3, table);
final ResultSet rs = stmt.executeQuery();
assertTrue(rs.next());
return rs.getString("TABLE_COLLATION");
} catch (NoDriverFoundException | SQLException exception) {
throw new IllegalStateException("Query '" + query + "' failed: " + exception.getMessage(), exception);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.exasol.dbbuilder.dialects.mysql;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import com.exasol.dbbuilder.dialects.DatabaseObjectWriter;
import com.exasol.dbbuilder.dialects.Schema;

@ExtendWith(MockitoExtension.class)
class MySqlTableTest {

@Mock
kaklakariada marked this conversation as resolved.
Show resolved Hide resolved
DatabaseObjectWriter writerMock;
@Mock
Schema schemaMock;

@Test
void createWithoutCharset() {
final MySqlTable table = MySqlTable.builder(writerMock, schemaMock, MySQLIdentifier.of("tableName")).build();
assertThat(table.getCharset(), is(nullValue()));
}

@Test
void createWithCharset() {
final MySqlTable table = MySqlTable.builder(writerMock, schemaMock, MySQLIdentifier.of("tableName"))
.charset("myCharset").build();
assertThat(table.getCharset(), equalTo("myCharset"));
}
}