Skip to content

Commit

Permalink
#134: Allowed specifying charset for MySQL tables
Browse files Browse the repository at this point in the history
  • Loading branch information
kaklakariada committed Sep 24, 2024
1 parent 2f4ab44 commit c565bf9
Show file tree
Hide file tree
Showing 9 changed files with 214 additions and 27 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
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.MySqlTableBuilder createTableBuilder(final String name) {
return MySqlTable.builder(getWriter(), this, getIdentifier(name));
}
}
51 changes: 51 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,51 @@
package com.exasol.dbbuilder.dialects.mysql;

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

public class MySqlTable extends Table {

private final String charset;

protected MySqlTable(final MySqlTableBuilder builder) {
super(builder);
this.charset = builder.charset;
}

public String getCharset() {
return charset;
}

public static MySqlTableBuilder builder(final DatabaseObjectWriter writer, final Schema parentSchema,
final Identifier tableName) {
return new MySqlTableBuilder(writer, parentSchema, tableName);
}

public static class MySqlTableBuilder extends Table.Builder {

private String charset;

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

@Override
public MySqlTableBuilder column(final String columnName, final String columnType) {
super.column(columnName, columnType);
return this;
}

public MySqlTableBuilder charset(final String charset) {
this.charset = charset;
return this;
}

@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
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 c565bf9

Please sign in to comment.