/*
* Copyright (c) 2017 OBiBa. All rights reserved.
*
* This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.obiba.magma.datasource.jdbc;
import com.google.common.base.Predicate;
import com.google.common.base.Strings;
import com.google.common.collect.*;
import liquibase.change.Change;
import liquibase.change.core.RenameTableChange;
import liquibase.database.Database;
import liquibase.database.DatabaseFactory;
import liquibase.database.DatabaseList;
import liquibase.database.ObjectQuotingStrategy;
import liquibase.database.jvm.JdbcConnection;
import liquibase.exception.LiquibaseException;
import liquibase.snapshot.DatabaseSnapshot;
import liquibase.snapshot.SnapshotControl;
import liquibase.snapshot.SnapshotGeneratorFactory;
import liquibase.sql.visitor.SqlVisitor;
import liquibase.statement.SqlStatement;
import liquibase.structure.core.Column;
import liquibase.structure.core.Table;
import liquibase.structure.core.View;
import org.obiba.magma.MagmaRuntimeException;
import org.obiba.magma.ValueTable;
import org.obiba.magma.ValueTableWriter;
import org.obiba.magma.datasource.jdbc.support.*;
import org.obiba.magma.support.AbstractDatasource;
import org.obiba.magma.support.Initialisables;
import org.obiba.magma.type.TextType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.ConnectionCallback;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate;
import javax.annotation.Nullable;
import javax.sql.DataSource;
import javax.validation.constraints.NotNull;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.*;
import static org.obiba.magma.datasource.jdbc.JdbcValueTableWriter.*;
import static org.obiba.magma.datasource.jdbc.support.TableUtils.newTable;
import static org.obiba.magma.datasource.jdbc.support.TableUtils.newView;
public class JdbcDatasource extends AbstractDatasource {
private static final Logger log = LoggerFactory.getLogger(JdbcDatasource.class);
private static final Set<String> RESERVED_NAMES = ImmutableSet
.of(VALUE_TABLES_TABLE, VARIABLES_TABLE, VARIABLE_ATTRIBUTES_TABLE, CATEGORIES_TABLE, CATEGORY_ATTRIBUTES_TABLE);
private static final String TYPE = "jdbc";
public static final String EPOCH = "1970-01-02 00:00:00.000";
private final JdbcTemplate jdbcTemplate;
private final NamedParameterJdbcTemplate namedParameterJdbcTemplate;
private final JdbcDatasourceSettings settings;
private DatabaseSnapshot snapshot;
private Map<String, String> valueTableMap;
private PlatformTransactionManager txManager;
private Database databaseTmpl;
private Map<String, String> escapedColumnNames = Maps.newConcurrentMap();
private Map<String, String> escapedTableNames = Maps.newConcurrentMap();
private final String ESC_ENTITY_TYPE_COLUMN, ESC_VALUE_TABLES_TABLE, ESC_DATASOURCE_COLUMN, ESC_NAME_COLUMN,
ESC_VALUE_TABLE_COLUMN, ESC_SQL_NAME_COLUMN;
@SuppressWarnings("ConstantConditions")
public JdbcDatasource(String name, @NotNull DataSource datasource, @NotNull JdbcDatasourceSettings settings,
PlatformTransactionManager txManager) {
super(name, TYPE);
if(settings == null) throw new IllegalArgumentException("null settings");
if(datasource == null) throw new IllegalArgumentException("null datasource");
this.settings = settings;
jdbcTemplate = new JdbcTemplate(datasource);
namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(datasource);
this.txManager = txManager;
ESC_ENTITY_TYPE_COLUMN = escapeColumnName(ENTITY_TYPE_COLUMN);
ESC_VALUE_TABLES_TABLE = escapeTableName(VALUE_TABLES_TABLE);
ESC_DATASOURCE_COLUMN = escapeColumnName(DATASOURCE_COLUMN);
ESC_NAME_COLUMN = escapeColumnName(NAME_COLUMN);
ESC_SQL_NAME_COLUMN = escapeColumnName(SQL_NAME_COLUMN);
ESC_VALUE_TABLE_COLUMN = escapeColumnName(VALUE_TABLE_COLUMN);
}
public JdbcDatasource(String name, @NotNull DataSource datasource, @NotNull JdbcDatasourceSettings settings) {
this(name, datasource, settings, new DataSourceTransactionManager(datasource));
}
public JdbcDatasource(String name, DataSource datasource, String defaultEntityType, boolean useMetadataTables,
PlatformTransactionManager txManager) {
this(name, datasource,
JdbcDatasourceSettings.newSettings(defaultEntityType).useMetadataTables(useMetadataTables).build(), txManager);
}
public JdbcDatasource(String name, DataSource datasource, String defaultEntityType, boolean useMetadataTables) {
this(name, datasource, defaultEntityType, useMetadataTables, new DataSourceTransactionManager(datasource));
}
//
// AbstractDatasource Methods
//
@Override
public boolean canDropTable(String tableName) {
return hasValueTable(tableName);
}
@Override
public void dropTable(@NotNull String tableName) {
JdbcValueTable table = (JdbcValueTable) getValueTable(tableName);
table.drop();
removeValueTable(table);
valueTableMap.remove(tableName);
}
@Override
public boolean canRenameTable(String tableName) {
return hasValueTable(tableName);
}
@Override
public void renameTable(String tableName, String newName) {
if(hasValueTable(newName)) throw new MagmaRuntimeException("A table already exists with the name: " + newName);
JdbcValueTable table = (JdbcValueTable) getValueTable(tableName);
if (table.isSQLView()) throw new MagmaRuntimeException("A SQL view cannot be renamed");
removeValueTable(table);
String newSqlName = getSettings().isUseMetadataTables() ? generateSqlTableName(newName) : newName;
getValueTableMap().remove(tableName);
getValueTableMap().put(newName, newSqlName);
doWithDatabase(
new ChangeDatabaseCallback(getTableRenameChanges(tableName, table.getSqlName(), newName, newSqlName)));
databaseChanged();
ValueTable vt = initialiseValueTable(newName);
Initialisables.initialise(vt);
addValueTable(vt);
}
@Override
public boolean canDrop() {
return true;
}
@Override
public void drop() {
for(ValueTable valueTable : ImmutableList.copyOf(getValueTables())) {
dropTable(valueTable.getName());
}
}
/**
* Returns a {@link ValueTableWriter} for writing to a new or existing {@link JdbcValueTable}.
* <p/>
* Note: Newly created tables have a single entity identifier column, "entity_id".
*/
@SuppressWarnings({ "AssignmentToMethodParameter", "PMD.AvoidReassigningParameters" })
@NotNull
@Override
public ValueTableWriter createWriter(@NotNull String tableName, @NotNull String entityType) {
//noinspection ConstantConditions
if(entityType == null) {
entityType = settings.getDefaultEntityType();
}
JdbcValueTable table;
if(hasValueTable(tableName)) {
table = (JdbcValueTable) getValueTable(tableName);
} else {
JdbcValueTableSettings tableSettings = settings.getTableSettingsForMagmaTable(tableName);
if(tableSettings == null) {
tableSettings = JdbcValueTableSettings.newSettings(generateSqlTableName(tableName))
.tableName(tableName) //
.entityType(entityType) //
.entityIdentifierColumn(settings.getDefaultEntityIdColumnName()).build();
}
table = new JdbcValueTable(this, tableSettings);
Initialisables.initialise(table);
addValueTable(table);
addTableMetaData(tableName, tableSettings);
}
return new JdbcValueTableWriter(table);
}
private void addTableMetaData(@NotNull String tableName, @NotNull JdbcValueTableSettings tableSettings) {
if(!getSettings().isUseMetadataTables()) return;
InsertDataChangeBuilder idc = InsertDataChangeBuilder.newBuilder() //
.tableName(VALUE_TABLES_TABLE);
if(getSettings().isMultipleDatasources()) idc.withColumn(DATASOURCE_COLUMN, getName());
idc.withColumn(NAME_COLUMN, tableName) //
.withColumn(ENTITY_TYPE_COLUMN, tableSettings.getEntityType()) //
.withColumn(CREATED_COLUMN, new Date()) //
.withColumn(UPDATED_COLUMN, new Date()) //
.withColumn(SQL_NAME_COLUMN, tableSettings.getSqlTableName());
doWithDatabase(new ChangeDatabaseCallback(idc.build()));
}
@Override
protected void onInitialise() {
if(getSettings().isUseMetadataTables()) {
createMetadataTablesIfNotPresent();
} else if (getSettings().hasTableSettingsFactories()) {
initialiseTableSettings();
}
}
@Override
protected void onDispose() {
}
@Override
protected Set<String> getValueTableNames() {
if(!getValueTableMap().isEmpty()) return getValueTableMap().keySet();
Set<String> names = getSettings().isUseMetadataTables()
? getRegisteredValueTableNames()
: getObservedValueTableNames();
for(String name : names) {
getValueTableMap().put(name, name);
}
return names;
}
@Override
protected ValueTable initialiseValueTable(String tableName) {
JdbcValueTableSettings tableSettings = settings.getTableSettingsForMagmaTable(tableName);
String sqlTableName = getValueTableMap().containsKey(tableName) ? getValueTableMap().get(tableName) : tableName;
String entityType = null;
if(getSettings().isUseMetadataTables()) {
String sql = getSettings().isMultipleDatasources()
? String.format("SELECT %s FROM %s WHERE %s = ? AND %s = ?", ESC_ENTITY_TYPE_COLUMN, ESC_VALUE_TABLES_TABLE,
ESC_DATASOURCE_COLUMN, ESC_NAME_COLUMN)
: String.format("SELECT %s FROM %s WHERE %s = ?", ESC_ENTITY_TYPE_COLUMN, ESC_VALUE_TABLES_TABLE,
ESC_NAME_COLUMN);
Object[] params = getSettings().isMultipleDatasources()
? new Object[] { getName(), tableName }
: new Object[] { tableName };
entityType = getJdbcTemplate().queryForObject(sql, params, String.class);
}
entityType = Strings.isNullOrEmpty(entityType) ? settings.getDefaultEntityType() : entityType;
if(tableSettings != null) return new JdbcValueTable(this, tableSettings);
Table table = getDatabaseSnapshot().get(newTable(sqlTableName));
if (table != null) {
return new JdbcValueTable(this, tableName, table, entityType);
}
View view = getDatabaseSnapshot().get(newView(sqlTableName));
if (view != null) {
return new JdbcValueTable(this, tableName, view, entityType, settings.getDefaultEntityIdColumnName());
}
return new JdbcValueTable(this,
JdbcValueTableSettings.newSettings(generateSqlTableName(tableName)) //
.tableName(tableName) //
.entityType(entityType) //
.entityIdentifierColumn(settings.getDefaultEntityIdColumnName()).build());
}
//
// Methods
//
@NotNull
private Set<String> getRegisteredValueTableNames() {
Set<String> names = new LinkedHashSet<>();
String select = getSettings().isMultipleDatasources() ? String
.format("SELECT %s FROM %s WHERE %s = '%s'", ESC_NAME_COLUMN, ESC_VALUE_TABLES_TABLE, ESC_DATASOURCE_COLUMN,
getName()) : String.format("SELECT %s FROM %s", ESC_NAME_COLUMN, ESC_VALUE_TABLES_TABLE);
names.addAll(getJdbcTemplate().query(select, (rs, rowNum) -> rs.getString(NAME_COLUMN)));
return names;
}
@NotNull
private Set<String> getObservedValueTableNames() {
Set<String> names = new LinkedHashSet<>();
getDatabaseSnapshot().get(Table.class).stream() //
.filter(table -> isTableIncluded(table.getName())) //
.forEach(table -> {
String tableName = table.getName();
List<JdbcValueTableSettings> tableSettings = settings.getTableSettingsForSqlTable(tableName);
if (tableSettings != null && !tableSettings.isEmpty()) {
tableSettings.forEach(settings -> names.add(settings.getMagmaTableName()));
} else if (JdbcValueTable.hasEntityIdentifierColumn(table, getSettings().getDefaultEntityIdColumnName())
|| !Strings.isNullOrEmpty(JdbcValueTable.getEntityIdentifierColumn(table))) {
names.add(tableName);
}
});
getDatabaseSnapshot().get(View.class).stream() //
.filter(view -> isTableIncluded(view.getName())) //
.forEach(view -> {
String viewName = view.getName();
List<JdbcValueTableSettings> tableSettings = settings.getTableSettingsForSqlTable(viewName);
if (tableSettings != null && !tableSettings.isEmpty()) {
tableSettings.forEach(settings -> names.add(settings.getMagmaTableName()));
} else if (JdbcValueTable.hasEntityIdentifierColumn(view, getSettings().getDefaultEntityIdColumnName())) {
names.add(viewName);
}
});
return names;
}
/**
* SQL Table (or View) is included if it has not a reserved name and it was filtered in by settings.
*
* @param sqlTableName
* @return
*/
private boolean isTableIncluded(String sqlTableName) {
return !RESERVED_NAMES.contains(sqlTableName.toLowerCase())
&& (settings.hasTableSettingsForSqlTable(sqlTableName) || !settings.hasMappedTables() || settings.hasMappedTable(sqlTableName));
}
/**
* Changes when a table is renamed.
*
* @param tableName
* @param sqlName
* @param newName
* @param newSqlName
*/
private List<Change> getTableRenameChanges(String tableName, String sqlName, String newName, String newSqlName) {
List<Change> changes = Lists.newArrayList();
if(!getSettings().isUseMetadataTables()) return changes;
String whereClause = getSettings().isMultipleDatasources()
? String.format("%s = '%s' AND %s = '%s'", ESC_DATASOURCE_COLUMN, getName(), ESC_NAME_COLUMN, tableName)
: String.format("%s = '%s'", ESC_NAME_COLUMN, tableName);
changes.add(UpdateDataChangeBuilder.newBuilder().tableName(VALUE_TABLES_TABLE) //
.withColumn(NAME_COLUMN, newName) //
.withColumn(SQL_NAME_COLUMN, newSqlName) //
.withColumn(UPDATED_COLUMN, new Date()) //
.where(whereClause).build());
whereClause = getSettings().isMultipleDatasources()
? String.format("%s = '%s' AND %s = '%s'", ESC_DATASOURCE_COLUMN, getName(), ESC_VALUE_TABLE_COLUMN, tableName)
: String.format("%s = '%s'", ESC_VALUE_TABLE_COLUMN, tableName);
changes.add(UpdateDataChangeBuilder.newBuilder().tableName(VARIABLES_TABLE) //
.withColumn(VALUE_TABLE_COLUMN, newName) //
.where(whereClause).build());
changes.add(UpdateDataChangeBuilder.newBuilder().tableName(VARIABLE_ATTRIBUTES_TABLE) //
.withColumn(VALUE_TABLE_COLUMN, newName) //
.where(whereClause).build());
changes.add(UpdateDataChangeBuilder.newBuilder().tableName(CATEGORIES_TABLE) //
.withColumn(VALUE_TABLE_COLUMN, newName) //
.where(whereClause).build());
changes.add(UpdateDataChangeBuilder.newBuilder().tableName(CATEGORY_ATTRIBUTES_TABLE) //
.withColumn(VALUE_TABLE_COLUMN, newName) //
.where(whereClause).build());
RenameTableChange rtc = new RenameTableChange();
rtc.setOldTableName(sqlName);
rtc.setNewTableName(newSqlName);
changes.add(rtc);
return changes;
}
private String generateSqlTableName(String tableName) {
return getSettings().isMultipleDatasources()
? String.format("%s_%s", TableUtils.normalize(getName()), TableUtils.normalize(tableName))
: TableUtils.normalize(tableName);
}
private Map<String, String> getValueTableMap() {
if(valueTableMap == null) {
valueTableMap = new HashMap<>();
if(getSettings().isUseMetadataTables()) {
String select = getSettings().isMultipleDatasources()
? String.format("SELECT %s, %s FROM %s WHERE %s = '%s'", ESC_NAME_COLUMN, ESC_SQL_NAME_COLUMN,
ESC_VALUE_TABLES_TABLE, ESC_DATASOURCE_COLUMN, getName())
: String.format("SELECT %s, %s FROM %s", ESC_NAME_COLUMN, ESC_SQL_NAME_COLUMN, ESC_VALUE_TABLES_TABLE);
List<Map.Entry<String, String>> entries = getJdbcTemplate() //
.query(select,
(rs, rowNum) -> Maps.immutableEntry(rs.getString(NAME_COLUMN), rs.getString(SQL_NAME_COLUMN)));
ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
entries.forEach(builder::put);
valueTableMap.putAll(builder.build());
}
}
return valueTableMap;
}
JdbcDatasourceSettings getSettings() {
return settings;
}
JdbcTemplate getJdbcTemplate() {
return jdbcTemplate;
}
NamedParameterJdbcTemplate getNamedParameterJdbcTemplate() {
return namedParameterJdbcTemplate;
}
TransactionTemplate getTransactionTemplate() {
TransactionTemplate txTemplate = new TransactionTemplate(txManager);
txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
return txTemplate;
}
String escapeTableName(final String identifier) {
if(!escapedTableNames.containsKey(identifier)) {
String escaped = doWithDatabase(new DatabaseCallback<String>() {
@Nullable
@Override
public String doInDatabase(Database database) throws LiquibaseException {
return database.escapeObjectName(identifier, Table.class);
}
});
escapedTableNames.put(identifier, escaped);
}
return escapedTableNames.get(identifier);
}
String escapeColumnName(final String identifier) {
if (!escapedColumnNames.containsKey(identifier)) {
String escaped = doWithDatabase(new DatabaseCallback<String>() {
@Nullable
@Override
public String doInDatabase(Database database) throws LiquibaseException {
return database.escapeObjectName(identifier, Column.class);
}
});
escapedColumnNames.put(identifier, escaped);
}
return escapedColumnNames.get(identifier);
}
DatabaseSnapshot getDatabaseSnapshot() {
if(snapshot == null) {
snapshot = doWithDatabase(database -> SnapshotGeneratorFactory.getInstance()
.createSnapshot(database.getDefaultSchema(), database, new SnapshotControl(database)));
}
return snapshot;
}
void databaseChanged() {
snapshot = null;
}
<T> T doWithDatabase(final DatabaseCallback<T> databaseCallback) {
return jdbcTemplate.execute(new ConnectionCallback<T>() {
@Nullable
@Override
public T doInConnection(Connection con) throws SQLException, DataAccessException {
try {
JdbcConnection jdbcCon = new JdbcConnection(con);
Database database = newDatabaseInstance(jdbcCon);
database.setConnection(jdbcCon);
database.setObjectQuotingStrategy(ObjectQuotingStrategy.QUOTE_ALL_OBJECTS);
return databaseCallback.doInDatabase(database);
} catch(LiquibaseException e) {
throw new SQLException(e);
}
}
});
}
private synchronized Database newDatabaseInstance(JdbcConnection jdbcCon) throws SQLException {
try {
if(databaseTmpl == null) {
databaseTmpl = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(jdbcCon);
}
return databaseTmpl.getClass().newInstance();
} catch(Exception e) {
throw new SQLException(e);
}
}
private void initialiseTableSettings() {
getSettings().getTableSettingsFactories().forEach(factory -> {
String escPartition = escapeColumnName(factory.getTablePartitionColumn());
List<String> filters = getJdbcTemplate().query(String
.format("SELECT DISTINCT %s FROM %s WHERE %s IS NOT NULL",
escPartition, escapeTableName(factory.getSqlTableName()), escPartition),
(rs, rowNum) -> rs.getObject(1).toString());
factory.createSettings(filters, this).forEach(settings -> getSettings().addTableSettings(settings));
});
}
private void createMetadataTablesIfNotPresent() {
List<Change> changes = new ArrayList<>();
createDatasourceMetadataTablesIfNotPresent(changes);
createVariableMetadataTablesIfNotPresent(changes);
createCategoryMetadataTablesIfNotPresent(changes);
doWithDatabase(new ChangeDatabaseCallback(changes));
}
private void createDatasourceMetadataTablesIfNotPresent(List<Change> changes) {
if(getDatabaseSnapshot().get(newTable(VALUE_TABLES_TABLE)) == null) {
CreateTableChangeBuilder builder = new CreateTableChangeBuilder()//
.tableName(VALUE_TABLES_TABLE);
if(getSettings().isMultipleDatasources()) builder.withColumn(DATASOURCE_COLUMN, "VARCHAR(255)").primaryKey();
builder.withColumn(NAME_COLUMN, "VARCHAR(255)").primaryKey() //
.withColumn(ENTITY_TYPE_COLUMN, "VARCHAR(255)").notNull() //
.withColumn(CREATED_COLUMN, "TIMESTAMP", EPOCH).notNull() //
.withColumn(UPDATED_COLUMN, "TIMESTAMP", EPOCH).notNull() //
.withColumn(SQL_NAME_COLUMN, "VARCHAR(255)").notNull();
changes.add(builder.build());
}
}
private void createVariableMetadataTablesIfNotPresent(List<Change> changes) {
Table sqlTable = getDatabaseSnapshot().get(newTable(VARIABLES_TABLE));
if(sqlTable == null) {
CreateTableChangeBuilder builder = new CreateTableChangeBuilder().tableName(VARIABLES_TABLE);
if(getSettings().isMultipleDatasources()) builder.withColumn(DATASOURCE_COLUMN, "VARCHAR(255)").primaryKey();
builder.withColumn(VALUE_TABLE_COLUMN, "VARCHAR(255)").primaryKey() //
.withColumn(NAME_COLUMN, "VARCHAR(255)").primaryKey() //
.withColumn(VALUE_TYPE_COLUMN, "VARCHAR(255)").notNull() //
.withColumn("ref_entity_type", "VARCHAR(255)") //
.withColumn("mime_type", "VARCHAR(255)") //
.withColumn("units", "VARCHAR(255)") //
.withColumn("is_repeatable", "BOOLEAN") //
.withColumn("occurrence_group", "VARCHAR(255)") //
.withColumn("index", "INT") //
.withColumn(SQL_NAME_COLUMN, "VARCHAR(255)").notNull();
changes.add(builder.build());
} else if (sqlTable.getColumn("ref_entity_type") == null) { // upgrade because of OPAL-2887
AddColumnChangeBuilder builder = new AddColumnChangeBuilder().table(VARIABLES_TABLE).column("ref_entity_type", "VARCHAR(255)");
changes.add(builder.build());
}
if(getDatabaseSnapshot().get(newTable(VARIABLE_ATTRIBUTES_TABLE)) == null) {
CreateTableChangeBuilder builder = new CreateTableChangeBuilder() //
.tableName(VARIABLE_ATTRIBUTES_TABLE);
if(getSettings().isMultipleDatasources()) builder.withColumn(DATASOURCE_COLUMN, "VARCHAR(255)");
builder.withColumn(VALUE_TABLE_COLUMN, "VARCHAR(255)") //
.withColumn(VARIABLE_COLUMN, "VARCHAR(255)") //
.withColumn(NAMESPACE_COLUMN, "VARCHAR(20)") //
.withColumn(NAME_COLUMN, "VARCHAR(255)") //
.withColumn(LOCALE_COLUMN, "VARCHAR(20)") //
.withColumn(VALUE_COLUMN, SqlTypes.sqlTypeFor(TextType.get(), SqlTypes.TEXT_TYPE_HINT_MEDIUM));
changes.add(builder.build());
}
}
private void createCategoryMetadataTablesIfNotPresent(List<Change> changes) {
if(getDatabaseSnapshot().get(newTable(CATEGORIES_TABLE)) == null) {
CreateTableChangeBuilder builder = new CreateTableChangeBuilder() //
.tableName(CATEGORIES_TABLE);
if(getSettings().isMultipleDatasources()) builder.withColumn(DATASOURCE_COLUMN, "VARCHAR(255)").primaryKey();
builder.withColumn(VALUE_TABLE_COLUMN, "VARCHAR(255)").primaryKey() //
.withColumn(VARIABLE_COLUMN, "VARCHAR(255)").primaryKey() //
.withColumn(NAME_COLUMN, "VARCHAR(255)").primaryKey() //
.withColumn(MISSING_COLUMN, "BOOLEAN").notNull();
changes.add(builder.build());
}
if(getDatabaseSnapshot().get(newTable(CATEGORY_ATTRIBUTES_TABLE)) == null) {
CreateTableChangeBuilder builder = new CreateTableChangeBuilder() //
.tableName(CATEGORY_ATTRIBUTES_TABLE);
if(getSettings().isMultipleDatasources()) builder.withColumn(DATASOURCE_COLUMN, "VARCHAR(255)");
builder.withColumn(VALUE_TABLE_COLUMN, "VARCHAR(255)") //
.withColumn(VARIABLE_COLUMN, "VARCHAR(255)") //
.withColumn(CATEGORY_COLUMN, "VARCHAR(255)") //
.withColumn(NAMESPACE_COLUMN, "VARCHAR(20)") //
.withColumn(NAME_COLUMN, "VARCHAR(255)") //
.withColumn(LOCALE_COLUMN, "VARCHAR(20)") //
.withColumn(VALUE_COLUMN, SqlTypes.sqlTypeFor(TextType.get(), SqlTypes.TEXT_TYPE_HINT_MEDIUM));
changes.add(builder.build());
}
}
/**
* Callback used for accessing the {@code Database} instance in a safe and consistent way.
*
* @param <T> the type of object returned by the callback if any
*/
interface DatabaseCallback<T> {
@Nullable
T doInDatabase(Database database) throws LiquibaseException;
}
/**
* An implementation of {@code DatabaseCallback} for issuing {@code Change} instances to the {@code Database}
*/
static class ChangeDatabaseCallback implements DatabaseCallback<Object> {
private final List<SqlVisitor> sqlVisitors;
private final Iterable<Change> changes;
ChangeDatabaseCallback(Change... changes) {
this(Arrays.asList(changes));
}
ChangeDatabaseCallback(Iterable<Change> changes) {
this(changes, Lists.newArrayList(new MySqlEngineVisitor()));
}
ChangeDatabaseCallback(Iterable<Change> changes, Iterable<? extends SqlVisitor> visitors) {
if(changes == null) throw new IllegalArgumentException("changes cannot be null");
if(visitors == null) throw new IllegalArgumentException("visitors cannot be null");
this.changes = changes;
sqlVisitors = ImmutableList.copyOf(visitors);
}
@Nullable
@Override
public Object doInDatabase(final Database database) throws LiquibaseException {
for(Change change : changes) {
if(log.isDebugEnabled()) {
for(SqlStatement st : change.generateStatements(database)) {
log.debug("Issuing statement: {}", st);
}
}
database.execute(change.generateStatements(database), getFilteredVisitors(database));
try {
database.commit(); //explicit commit needed for postgres.
} catch(Exception e) {
if(!e.getMessage().contains("Commit can not be set while enrolled in a transaction")) throw e;
}
}
return null;
}
private List<SqlVisitor> getFilteredVisitors(final Database database) {
return Lists.newArrayList(Iterables.filter(sqlVisitors, new Predicate<SqlVisitor>() {
@Override
public boolean apply(@Nullable SqlVisitor input) {
return DatabaseList.definitionMatches(input.getApplicableDbms(), database.getShortName(), true);
}
}));
}
}
}