Skip to content

Commit

Permalink
SONAR-8025 add support for autoincrement column to CreateTableBuilder
Browse files Browse the repository at this point in the history
  • Loading branch information
sns-seb committed Sep 16, 2016
1 parent 9b2b69b commit 0fe16d8
Show file tree
Hide file tree
Showing 3 changed files with 324 additions and 25 deletions.
125 changes: 118 additions & 7 deletions sonar-db/src/main/java/org/sonar/db/version/CreateTableBuilder.java
Expand Up @@ -19,24 +19,35 @@
*/
package org.sonar.db.version;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.stream.Stream;
import javax.annotation.CheckForNull;
import org.sonar.core.util.stream.Collectors;
import org.sonar.db.dialect.Dialect;
import org.sonar.db.dialect.H2;
import org.sonar.db.dialect.MsSql;
import org.sonar.db.dialect.MySql;
import org.sonar.db.dialect.Oracle;
import org.sonar.db.dialect.PostgreSql;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Stream.of;

public class CreateTableBuilder {
private final Dialect dialect;
private final String tableName;
private final List<ColumnDef> columnDefs = new ArrayList<>();
private final List<ColumnDef> pkColumnDefs = new ArrayList<>(2);
private final Multimap<ColumnDef, ColumnFlag> flagsByColumn = HashMultimap.create(1, 1);
@CheckForNull
private String pkConstraintName;

Expand All @@ -48,19 +59,68 @@ public CreateTableBuilder(Dialect dialect, String tableName) {
public List<String> build() {
checkState(!columnDefs.isEmpty() || !pkColumnDefs.isEmpty(), "at least one column must be specified");

return Collections.singletonList(createTableStatement());
return Stream.concat(of(createTableStatement()), createOracleAutoIncrementStatements())
.collect(Collectors.toList());
}

private Stream<String> createOracleAutoIncrementStatements() {
if (!Oracle.ID.equals(dialect.getId())) {
return Stream.empty();
}
return pkColumnDefs.stream()
.filter(this::isAutoIncrement)
.flatMap(columnDef -> of(createSequenceFor(tableName), createTriggerFor(tableName)));
}

private String createSequenceFor(String tableName) {
return "CREATE SEQUENCE " + tableName + "_seq START WITH 1 INCREMENT BY 1";
}

private static String createTriggerFor(String tableName) {
return "CREATE OR REPLACE TRIGGER " + tableName + "_idt" +
" BEFORE INSERT ON " + tableName +
" FOR EACH ROW" +
" BEGIN" +
" IF :new.id IS null THEN" +
" SELECT " + tableName + "_seq.nextval INTO :new.id FROM dual;" +
" END IF;" +
" END;";
}

public CreateTableBuilder addColumn(ColumnDef columnDef) {
columnDefs.add(requireNonNull(columnDef, "column def can't be null"));
return this;
}

public CreateTableBuilder addPkColumn(ColumnDef columnDef) {
public CreateTableBuilder addPkColumn(ColumnDef columnDef, ColumnFlag... flags) {
pkColumnDefs.add(requireNonNull(columnDef, "column def can't be null"));
addFlags(columnDef, flags);
return this;
}

private void addFlags(ColumnDef columnDef, ColumnFlag[] flags) {
Arrays.stream(flags)
.forEach(flag -> {
requireNonNull(flag, "flag can't be null");
if (flag == ColumnFlag.AUTO_INCREMENT) {
validateColumnDefForAutoIncrement(columnDef);
}
flagsByColumn.put(columnDef, flag);
});
}

private void validateColumnDefForAutoIncrement(ColumnDef columnDef) {
checkArgument("id".equals(columnDef.getName()),
"Auto increment column name must be id");
checkArgument(columnDef instanceof BigIntegerColumnDef
|| columnDef instanceof IntegerColumnDef,
"Auto increment column must either be BigInteger or Integer");
checkArgument(!columnDef.isNullable(),
"Auto increment column can't be nullable");
checkState(pkColumnDefs.stream().filter(this::isAutoIncrement).count() == 0,
"There can't be more than one auto increment column");
}

public CreateTableBuilder withPkConstraintName(String pkConstraintName) {
this.pkConstraintName = requireNonNull(pkConstraintName, "primary key constraint name can't be null");
return this;
Expand All @@ -85,29 +145,76 @@ private void addPkColumns(StringBuilder res) {
}
}

private static void addColumns(StringBuilder res, Dialect dialect, List<ColumnDef> columnDefs) {
private void addColumns(StringBuilder res, Dialect dialect, List<ColumnDef> columnDefs) {
if (columnDefs.isEmpty()) {
return;
}
Iterator<ColumnDef> columnDefIterator = columnDefs.iterator();
while (columnDefIterator.hasNext()) {
ColumnDef columnDef = columnDefIterator.next();
res.append(columnDef.getName()).append(' ').append(columnDef.generateSqlType(dialect));
addNullConstraint(res, columnDef);
res.append(columnDef.getName());
res.append(' ');
appendDataType(res, dialect, columnDef);
appendNullConstraint(res, columnDef);
appendColumnFlags(res, dialect, columnDef);
if (columnDefIterator.hasNext()) {
res.append(',');
}
}
}

private static void addNullConstraint(StringBuilder res, ColumnDef columnDef) {
private void appendDataType(StringBuilder res, Dialect dialect, ColumnDef columnDef) {
if (PostgreSql.ID.equals(dialect.getId()) && isAutoIncrement(columnDef)) {
if (columnDef instanceof BigIntegerColumnDef) {
res.append("BIGSERIAL");
} else if (columnDef instanceof IntegerColumnDef) {
res.append("SERIAL");
} else {
throw new IllegalStateException("Column with autoincrement is neither BigInteger nor Integer");
}
} else {
res.append(columnDef.generateSqlType(dialect));
}
}

private boolean isAutoIncrement(ColumnDef columnDef) {
Collection<ColumnFlag> columnFlags = this.flagsByColumn.get(columnDef);
return columnFlags != null && columnFlags.contains(ColumnFlag.AUTO_INCREMENT);
}

private static void appendNullConstraint(StringBuilder res, ColumnDef columnDef) {
if (columnDef.isNullable()) {
res.append(" NULL");
} else {
res.append(" NOT NULL");
}
}

private void appendColumnFlags(StringBuilder res, Dialect dialect, ColumnDef columnDef) {
Collection<ColumnFlag> columnFlags = this.flagsByColumn.get(columnDef);
if (columnFlags != null && columnFlags.contains(ColumnFlag.AUTO_INCREMENT)) {
switch (dialect.getId()) {
case Oracle.ID:
// no auto increment on Oracle, must use a sequence
break;
case PostgreSql.ID:
// no specific clause on PostgreSQL but a specific type
break;
case MsSql.ID:
res.append(" IDENTITY (0,1)");
break;
case MySql.ID:
res.append(" AUTO_INCREMENT");
break;
case H2.ID:
res.append(" AUTO_INCREMENT (0,1)");
break;
default:
throw new IllegalArgumentException("Unsupported dialect id " + dialect.getId());
}
}
}

private void addPkConstraint(StringBuilder res) {
if (pkColumnDefs.isEmpty()) {
return;
Expand Down Expand Up @@ -155,4 +262,8 @@ private void addLOBStorageClause(StringBuilder res, Dialect dialect, List<Column
}
}

public enum ColumnFlag {
AUTO_INCREMENT
}

}
Expand Up @@ -19,29 +19,39 @@
*/
package org.sonar.db.version;

import java.util.Map;
import org.junit.ClassRule;
import org.junit.Test;
import org.sonar.api.utils.System2;
import org.sonar.db.DbTester;
import org.sonar.db.dialect.Dialect;

import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
import static org.sonar.db.version.BigIntegerColumnDef.newBigIntegerColumnDefBuilder;
import static org.sonar.db.version.BlobColumnDef.newBlobColumnDefBuilder;
import static org.sonar.db.version.BooleanColumnDef.newBooleanColumnDefBuilder;
import static org.sonar.db.version.ClobColumnDef.newClobColumnDefBuilder;
import static org.sonar.db.version.CreateTableBuilder.ColumnFlag.AUTO_INCREMENT;
import static org.sonar.db.version.DecimalColumnDef.newDecimalColumnDefBuilder;
import static org.sonar.db.version.IntegerColumnDef.newIntegerColumnDefBuilder;
import static org.sonar.db.version.VarcharColumnDef.newVarcharColumnDefBuilder;

public class CreateTableBuilderDbTesterTest {
@ClassRule
public static final DbTester dbTester = DbTester.create(System2.INSTANCE);

private Dialect dialect = dbTester.getDbClient().getDatabase().getDialect();
private static int tableNameGenerator = 0;

@Test
public void create_no_primary_key_table() {
String createTableStmt = new CreateTableBuilder(dbTester.getDbClient().getDatabase().getDialect(), "TABLE_1")
newCreateTableBuilder()
.addColumn(newBooleanColumnDefBuilder().setColumnName("bool_col_1").build())
.addColumn(newBooleanColumnDefBuilder().setColumnName("bool_col_2").setIsNullable(false).build())
.addColumn(newBigIntegerColumnDefBuilder().setColumnName("bg_col_1").build())
.addColumn(newBigIntegerColumnDefBuilder().setColumnName("bg_col_2").setIsNullable(false).build())
.addColumn(newIntegerColumnDefBuilder().setColumnName("i_col_1").build())
.addColumn(newIntegerColumnDefBuilder().setColumnName("i_col_2").setIsNullable(false).build())
.addColumn(newBigIntegerColumnDefBuilder().setColumnName("bi_col_1").build())
.addColumn(newBigIntegerColumnDefBuilder().setColumnName("bi_col_2").setIsNullable(false).build())
.addColumn(newClobColumnDefBuilder().setColumnName("clob_col_1").build())
.addColumn(newClobColumnDefBuilder().setColumnName("clob_col_2").setIsNullable(false).build())
.addColumn(newDecimalColumnDefBuilder().setColumnName("dec_col_1").build())
Expand All @@ -53,31 +63,70 @@ public void create_no_primary_key_table() {
.addColumn(newBlobColumnDefBuilder().setColumnName("blob_col_1").build())
.addColumn(newBlobColumnDefBuilder().setColumnName("blob_col_2").setIsNullable(false).build())
.build()
.iterator().next();

dbTester.executeDdl(createTableStmt);
.forEach(dbTester::executeDdl);
}

@Test
public void create_single_column_primary_key_table() {
String createTableStmt = new CreateTableBuilder(dbTester.getDbClient().getDatabase().getDialect(), "TABLE_2")
newCreateTableBuilder()
.addPkColumn(newBigIntegerColumnDefBuilder().setColumnName("bg_col_1").setIsNullable(false).build())
.addColumn(newVarcharColumnDefBuilder().setColumnName("varchar_col_2").setLimit(40).setIsNullable(false).build())
.build()
.iterator().next();

dbTester.executeDdl(createTableStmt);
.forEach(dbTester::executeDdl);
}

@Test
public void create_multi_column_primary_key_table() {
String createTableStmt = new CreateTableBuilder(dbTester.getDbClient().getDatabase().getDialect(), "TABLE_3")
newCreateTableBuilder()
.addPkColumn(newBigIntegerColumnDefBuilder().setColumnName("bg_col_1").setIsNullable(false).build())
.addPkColumn(newBigIntegerColumnDefBuilder().setColumnName("bg_col_2").setIsNullable(false).build())
.addColumn(newVarcharColumnDefBuilder().setColumnName("varchar_col_2").setLimit(40).setIsNullable(false).build())
.build()
.iterator().next();
.forEach(dbTester::executeDdl);
}

@Test
public void create_autoincrement_notnullable_integer_primary_key_table() {
String tableName = createTableName();
new CreateTableBuilder(dialect, tableName)
.addPkColumn(newIntegerColumnDefBuilder().setColumnName("id").setIsNullable(false).build(), AUTO_INCREMENT)
.addColumn(valColumnDef())
.build()
.forEach(dbTester::executeDdl);

verifyAutoIncrementIsWorking(tableName);
}

@Test
public void create_autoincrement_notnullable_biginteger_primary_key_table() {
String tableName = createTableName();
new CreateTableBuilder(dialect, tableName)
.addPkColumn(newBigIntegerColumnDefBuilder().setColumnName("id").setIsNullable(false).build(), AUTO_INCREMENT)
.addColumn(valColumnDef())
.build()
.forEach(dbTester::executeDdl);

verifyAutoIncrementIsWorking(tableName);
}

private static VarcharColumnDef valColumnDef() {
return newVarcharColumnDefBuilder().setColumnName("val").setLimit(10).setIsNullable(false).build();
}

private void verifyAutoIncrementIsWorking(String tableName) {
dbTester.executeInsert(tableName, "val", "toto");
dbTester.commit();

Map<String, Object> row = dbTester.selectFirst("select id as \"id\", val as \"val\" from " + tableName);
assertThat(row.get("id")).isNotNull();
assertThat(row.get("val")).isEqualTo("toto");
}

private CreateTableBuilder newCreateTableBuilder() {
return new CreateTableBuilder(dialect, createTableName());
}

dbTester.executeDdl(createTableStmt);
private static String createTableName() {
return "table_" + tableNameGenerator++;
}
}

0 comments on commit 0fe16d8

Please sign in to comment.