Skip to content

Commit

Permalink
#134 MySql table charset (#141)
Browse files Browse the repository at this point in the history
Co-authored-by: Sebastian Bär <[email protected]>
  • Loading branch information
kaklakariada and redcatbear authored Sep 24, 2024
1 parent 6985f32 commit 53de628
Show file tree
Hide file tree
Showing 10 changed files with 251 additions and 29 deletions.
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));
}
}
87 changes: 87 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,87 @@
package com.exasol.dbbuilder.dialects.mysql;

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

/**
* A MySql table that allows specifying a character set.
*/
public class MySqlTable extends Table {
private final String charset;

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

/**
* Get the table's character set.
*
* @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;

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

@Override
// Overriding this so that returned builder has the right type and users don't need to cast.
public Builder column(final String columnName, final String columnType) {
return (Builder) super.column(columnName, columnType);
}

/**
* Set a custom character set for the new table. Defaults to UTF-8.
* <p>
* This character set is then used for the whole table down to the columns. Additionally the standard collation
* rules for this dataset are applied.
* </p>
*
* @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,33 @@
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
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"));
}
}

0 comments on commit 53de628

Please sign in to comment.