package liquibase.changelog;

import liquibase.ContextExpression;
import liquibase.Contexts;
import liquibase.LabelExpression;
import liquibase.Labels;
import liquibase.configuration.LiquibaseConfiguration;
import liquibase.database.Database;
import liquibase.database.DatabaseList;
import liquibase.exception.DatabaseException;
import liquibase.parser.ChangeLogParserCofiguration;
import liquibase.util.StringUtil;

import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class ChangeLogParameters {

    private List<ChangeLogParameter> changeLogParameters = new ArrayList<>();
    private ExpressionExpander expressionExpander;
    private Database currentDatabase;
    private Contexts currentContexts;
    private LabelExpression currentLabelExpression;

    public ChangeLogParameters() {
        this(null);
    }

    public ChangeLogParameters(Database database) {
    	LinkedHashMap<Object, Object> externalParameters = new LinkedHashMap<>();
    	// First add environment variables
    	externalParameters.putAll(System.getenv());

    	// Next add system properties; they have higher precedence than / overwrite environment variables
    	externalParameters.putAll((Properties) System.getProperties().clone());

    	for (Map.Entry entry : externalParameters.entrySet()) {
            changeLogParameters.add(new ChangeLogParameter(entry.getKey().toString(), entry.getValue()));
        }

        if (database != null) {
            this.set("database.autoIncrementClause", database.getAutoIncrementClause(null, null));
            this.set("database.currentDateTimeFunction", database.getCurrentDateTimeFunction());
            this.set("database.databaseChangeLogLockTableName", database.getDatabaseChangeLogLockTableName());
            this.set("database.databaseChangeLogTableName", database.getDatabaseChangeLogTableName());
            try {
                this.set("database.databaseMajorVersion", database.getDatabaseMajorVersion());
            } catch (DatabaseException ignore) {
            }
            try {
                this.set("database.databaseMinorVersion", database.getDatabaseMinorVersion());
            } catch (DatabaseException ignore) {
            }
            this.set("database.databaseProductName", database.getDatabaseProductName());
            try {
                this.set("database.databaseProductVersion", database.getDatabaseProductVersion());
            } catch (DatabaseException ignore) {
            }
            this.set("database.defaultCatalogName", database.getDefaultCatalogName());
            this.set("database.defaultSchemaName", database.getDefaultSchemaName());
            this.set("database.defaultSchemaNamePrefix", (StringUtil.trimToNull(database.getDefaultSchemaName()) ==
                null) ? "" : ("." + database.getDefaultSchemaName()));
            this.set("database.lineComment", database.getLineComment());
            this.set("database.liquibaseSchemaName", database.getLiquibaseSchemaName());
            this.set("database.typeName", database.getShortName());
            try {
                this.set("database.isSafeToRunUpdate", database.isSafeToRunUpdate());
            } catch (DatabaseException ignore) {
            }
            this.set("database.requiresPassword", database.requiresPassword());
            this.set("database.requiresUsername", database.requiresUsername());
            this.set("database.supportsForeignKeyDisable", database.supportsForeignKeyDisable());
            this.set("database.supportsInitiallyDeferrableColumns", database.supportsInitiallyDeferrableColumns());
            this.set("database.supportsRestrictForeignKeys", database.supportsRestrictForeignKeys());
            this.set("database.supportsSchemas", database.supportsSchemas());
            this.set("database.supportsSequences", database.supportsSequences());
            this.set("database.supportsTablespaces", database.supportsTablespaces());
            this.set("database.supportsNotNullConstraintNames", database.supportsNotNullConstraintNames());
        }

        this.expressionExpander = new ExpressionExpander(this);
        this.currentDatabase = database;
        this.currentContexts = new Contexts();
        this.currentLabelExpression = new LabelExpression();
    }

    public void setContexts(Contexts contexts) {
        this.currentContexts = contexts;
    }

    public Contexts getContexts() {
        return currentContexts;
    }

    public List<ChangeLogParameter> getChangeLogParameters() {
        return Collections.unmodifiableList(changeLogParameters);
    }

    public void set(String paramter, Object value) {
        /*
         * TODO: this was a bug. Muliple created parameters have been created, but the corresponding method in
         * #findParameter() is only catching the first one. So here we should eliminate duplicate entries
         */
        ChangeLogParameter param = findParameter(paramter, null);
        if (param == null) {
            // okay add it
            changeLogParameters.add(new ChangeLogParameter(paramter, value));
        }
    }

    public void set(String key, String value, String contexts, String labels, String databases, boolean globalParam,
                    DatabaseChangeLog changeLog) {
        set(key, value, new ContextExpression(contexts), new Labels(labels), databases, globalParam, changeLog);
    }

    public void set(String key, String value, ContextExpression contexts, Labels labels, String databases,
                    boolean globalParam, DatabaseChangeLog changeLog) {
        /**
         * TODO: this was a bug. Muliple created parameters have been created, but the corresponding method in
         * #findParameter() is only catching the first one. So here we should eliminate duplicate entries
         **/
        if (globalParam) {
            // if it is global param ignore additional adds
            ChangeLogParameter param = findParameter(key, null);
            if (param == null) {
                // okay add it
                changeLogParameters.add(new ChangeLogParameter(key, value, contexts, labels, databases, globalParam,
                    changeLog));
            }
        } else {
            //this is a non-global param, just add it
            changeLogParameters.add(new ChangeLogParameter(key, value, contexts, labels, databases, globalParam,
                changeLog));
        }
    }

    /**
     * Return the value of a parameter
     *
     * @param key Name of the parameter
     * @return The parameter value or null if not found. (Note that null can also be return if it is the parameter
     * value. For strict parameter existence use {@link #hasValue(String, DatabaseChangeLog)}
     */
    public Object getValue(String key, DatabaseChangeLog changeLog) {
        ChangeLogParameter parameter = findParameter(key, changeLog);
        return (parameter != null) ? parameter.getValue() : null;
    }

    private ChangeLogParameter findParameter(String key, DatabaseChangeLog changeLog) {
        ChangeLogParameter result = null;

        List<ChangeLogParameter> found = new ArrayList<>();
        for (ChangeLogParameter param : changeLogParameters) {
            if (param.getKey().equalsIgnoreCase(key) && param.isValid()) {
                found.add(param);
            }
        }

        if (found.size() == 1) {
            // this case is typically a global param, but could also be a unique non-global param in one specific
            // changelog
            result = found.get(0);
        } else if (found.size() > 1) {
            for (ChangeLogParameter changeLogParameter : found) {
                if (changeLogParameter.getChangeLog().equals(changeLog)) {
                    result = changeLogParameter;
                }
            }
        }

        return result;
    }

    public boolean hasValue(String key, DatabaseChangeLog changeLog) {
        return findParameter(key, changeLog) != null;
    }

    public String expandExpressions(String string, DatabaseChangeLog changeLog) {
        return expressionExpander.expandExpressions(string, changeLog);
    }

    public void setLabels(LabelExpression labels) {
        this.currentLabelExpression = labels;
    }

    public LabelExpression getLabels() {
        return currentLabelExpression;
    }

    protected static class ExpressionExpander {
        private boolean enableEscaping;
        private ChangeLogParameters changeLogParameters;
        private static final Pattern EXPRESSION_PATTERN = Pattern.compile("(\\$\\{[^\\}]+\\})");

        public ExpressionExpander(ChangeLogParameters changeLogParameters) {
            this.changeLogParameters = changeLogParameters;
            this.enableEscaping = LiquibaseConfiguration.getInstance()
                .getConfiguration(ChangeLogParserCofiguration.class).getSupportPropertyEscaping();
        }

        public String expandExpressions(String text, DatabaseChangeLog changeLog) {
            if (text == null) {
                return null;
            }
            Matcher matcher = EXPRESSION_PATTERN.matcher(text);
            String originalText = text;
            while (matcher.find()) {
                String expressionString = originalText.substring(matcher.start(), matcher.end());
                String valueTolookup = expressionString.replaceFirst("\\$\\{", "").replaceFirst("\\}$", "");

                Object value = (enableEscaping && valueTolookup.startsWith(":")) ? null : changeLogParameters
                    .getValue(valueTolookup, changeLog);

                if (value != null) {
                    text = text.replace(expressionString, value.toString());
                }
            }

            // replace all escaped expressions with its literal
            if (enableEscaping) {
                text = text.replaceAll("\\$\\{:(.+?)}", "\\$\\{$1}");
            }

            return text;
        }
    }

    public class ChangeLogParameter {
        private String key;
        private Object value;
        private ContextExpression validContexts;
        private Labels labels;
        private List<String> validDatabases;
        /** is this parameter a global parameter, means globally over all changesets. */
        private boolean global = true;
        private DatabaseChangeLog changeLog;

        public ChangeLogParameter(String key, Object value) {
            this.key = key;
            this.value = value;
        }

        public ChangeLogParameter(String key, Object value, String validContexts, String labels, String validDatabases,
                                  boolean globalParam, DatabaseChangeLog changeLog) {
            this(key, value, new ContextExpression(validContexts), new Labels(labels),
                StringUtil.splitAndTrim(validDatabases, ","), globalParam, changeLog);
        }

        private ChangeLogParameter(String key, Object value, ContextExpression validContexts, Labels labels,
                                   String validDatabases, boolean globalParam, DatabaseChangeLog changeLog) {
            this(key, value, validContexts, labels, StringUtil.splitAndTrim(validDatabases, ","),
                globalParam, changeLog);
        }

        public ChangeLogParameter(String key, Object value, ContextExpression validContexts, Labels labels,
                                  List<String> validDatabases, boolean globalParam, DatabaseChangeLog changeLog) {
            this.key = key;
            this.value = value;
            this.validContexts = validContexts;
            this.labels = labels;
            this.validDatabases = validDatabases;
            this.global = globalParam;
            this.changeLog = changeLog;
        }

        public String getKey() {
            return key;
        }

        public Object getValue() {
            return value;
        }

        public List<String> getValidDatabases() {
            return validDatabases;
        }

        public ContextExpression getValidContexts() {
            return validContexts;
        }

        public Labels getLabels() {
            return labels;
        }

        @Override
        public String toString() {
            return getValue().toString();
        }

        public boolean isValid() {
            boolean isValid = (validContexts == null)
                || validContexts.matches(ChangeLogParameters.this.currentContexts);

            if (isValid) {
                isValid = (labels == null) || (currentLabelExpression == null)
                    || currentLabelExpression.matches(labels);
            }

            if (isValid) {
                isValid = DatabaseList.definitionMatches(validDatabases, currentDatabase, true);
            }

            return isValid;
        }

        public boolean isGlobal() {
            return global;
        }

        public DatabaseChangeLog getChangeLog() {
            return changeLog;
        }
    }
}


package liquibase.database;

import liquibase.util.StringUtil;

import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

public class DatabaseList {
    /**
     * Compares a given database to a database definition string.
     *

     * Definition syntax: Comma separated list of database shortNames. Doesn't matter if it includes spaces.
     * Can exclude a database by prepending its name with a '!'.
     * The string "all" will match all databases. The string "none" will match no databases, even if others are listed.
     * If an empty or null definition or null is passed, it will return the passed returnValueIfEmpty value.
     */
    public static boolean definitionMatches(String definition, String databaseShortName, boolean returnValueIfEmpty) {
        return definitionMatches(StringUtil.splitAndTrim(StringUtil.trimToNull(definition), ","), databaseShortName, returnValueIfEmpty);
    }

    public static boolean definitionMatches(String definition, Database database, boolean returnValueIfEmpty) {
        return definitionMatches(StringUtil.splitAndTrim(StringUtil.trimToNull(definition), ","), database, returnValueIfEmpty);
    }

    /**
     * Same logic as {@link #definitionMatches(String, liquibase.database.Database, boolean)} but with a collection of definitions rather than a comma separated list.
     */
    public static boolean definitionMatches(Collection<String> definition, String databaseShortName, boolean returnValueIfEmptyList) {
        if ((definition == null) || definition.isEmpty()) {
            return returnValueIfEmptyList;
        }

        if (definition.contains("all")) {
            return true;
        }

        if (definition.contains("none")) {
            return false;
        }

        // !h2 would mean that the h2 database should be excluded
        if (definition.contains("!" + databaseShortName)) {
            return false;
        }

        Set<String> dbmsSupported = new HashSet<>();
        // add all dbms that do not start with ! to a list
        for (String dbms: definition) {
            if (!dbms.startsWith("!")) {
                dbmsSupported.add(dbms);
            }
        }

        if (dbmsSupported.isEmpty() || dbmsSupported.contains(databaseShortName)) {
            return true;
        }

        return false;

    }

    public static boolean definitionMatches(Collection<String> definition, Database database, boolean returnValueIfEmptyList) {
        String shortName;
        if (database == null) {
            shortName = "null";
        } else {
            shortName = database.getShortName();
        }
        return definitionMatches(definition, shortName, returnValueIfEmptyList);
    }

    public static Set<String> toDbmsSet(String dbmsList) {
        Set<String> dbmsSet = null;
        if (StringUtil.trimToNull(dbmsList) != null) {
            dbmsSet = new HashSet<String>();
            for (String string : dbmsList.toLowerCase().split(",")) {
                dbmsSet.add(string.trim());
            }
        }
        return dbmsSet;
    }
}


package liquibase.snapshot.jvm;

import liquibase.Scope;
import liquibase.database.AbstractJdbcDatabase;
import liquibase.database.Database;
import liquibase.database.OfflineConnection;
import liquibase.database.core.*;
import liquibase.database.jvm.JdbcConnection;
import liquibase.exception.DatabaseException;
import liquibase.exception.UnexpectedLiquibaseException;
import liquibase.executor.Executor;
import liquibase.executor.ExecutorService;
import liquibase.logging.LogType;
import liquibase.logging.Logger;
import liquibase.snapshot.CachedRow;
import liquibase.snapshot.DatabaseSnapshot;
import liquibase.snapshot.InvalidExampleException;
import liquibase.snapshot.JdbcDatabaseSnapshot;
import liquibase.statement.DatabaseFunction;
import liquibase.statement.core.RawSqlStatement;
import liquibase.structure.DatabaseObject;
import liquibase.structure.core.*;
import liquibase.util.SqlUtil;
import liquibase.util.StringUtil;

import java.sql.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class ColumnSnapshotGenerator extends JdbcSnapshotGenerator {

    private static final String LIQUIBASE_COMPLETE = "liquibase-complete";
    protected static final String COLUMN_DEF_COL = "COLUMN_DEF";

    private Pattern postgresStringValuePattern = Pattern.compile("'(.*)'::[\\w ]+");
    private Pattern postgresNumberValuePattern = Pattern.compile("(\\d*)::[\\w ]+");

    public ColumnSnapshotGenerator() {
        super(Column.class, new Class[]{Table.class, View.class});
    }

    @Override
    protected DatabaseObject snapshotObject(DatabaseObject example, DatabaseSnapshot snapshot) throws DatabaseException, InvalidExampleException {
        if ((((Column) example).getComputed() != null) && ((Column) example).getComputed()) {
            return example;
        }
        Relation relation = ((Column) example).getRelation();
        Schema schema = relation.getSchema();

        List<CachedRow> columnMetadataRs = null;
        try {

            Column column = null;

            if (example.getAttribute(LIQUIBASE_COMPLETE, false)) {
                column = (Column) example;
                example.setAttribute(LIQUIBASE_COMPLETE, null);
            } else {
                JdbcDatabaseSnapshot.CachingDatabaseMetaData databaseMetaData =
                    ((JdbcDatabaseSnapshot) snapshot).getMetaDataFromCache();

                Database database = snapshot.getDatabase();
                columnMetadataRs = databaseMetaData.getColumns(
                        ((AbstractJdbcDatabase) database).getJdbcCatalogName(schema),
                        ((AbstractJdbcDatabase) database).getJdbcSchemaName(schema),
                        relation.getName(),
                        example.getName()
                );

                if (!columnMetadataRs.isEmpty()) {
                    CachedRow data = columnMetadataRs.get(0);
                    column = readColumn(data, relation, database);
                    setAutoIncrementDetails(column, database, snapshot);
                }
            }

            return column;
        } catch (DatabaseException|SQLException e) {
            throw new DatabaseException(e);
        }
    }

    @Override
    protected void addTo(DatabaseObject foundObject, DatabaseSnapshot snapshot) throws DatabaseException, InvalidExampleException {
        if (!snapshot.getSnapshotControl().shouldInclude(Column.class)) {
            return;
        }
        if (foundObject instanceof Relation) {
            Database database = snapshot.getDatabase();
            Relation relation = (Relation) foundObject;
            List<CachedRow> allColumnsMetadataRs = null;
            try {

                JdbcDatabaseSnapshot.CachingDatabaseMetaData databaseMetaData =
                    ((JdbcDatabaseSnapshot) snapshot).getMetaDataFromCache();

                Schema schema;

                schema = relation.getSchema();
                allColumnsMetadataRs = databaseMetaData.getColumns(
                        ((AbstractJdbcDatabase) database).getJdbcCatalogName(schema),
                        ((AbstractJdbcDatabase) database).getJdbcSchemaName(schema),
                        relation.getName(),
                        null);

                /*
                 * Microsoft SQL Server, SAP SQL Anywhere and probably other RDBMS guarantee non-duplicate
                 * ORDINAL_POSITIONs for the columns of a single table. But they do not guarantee there are no gaps
                 * in that integers (e.g. if columns have been deleted). So we need to check for that and renumber
                 * if needed.
                 */
                TreeMap<Integer, CachedRow> treeSet = new TreeMap<>();
                for (CachedRow row : allColumnsMetadataRs) {
                    treeSet.put(row.getInt("ORDINAL_POSITION"), row);
                }
                Logger log = Scope.getCurrentScope().getLog(getClass());

                // Now we can iterate through the sorted list and repair if needed.
                int currentOrdinal = 0;
                for (CachedRow row : treeSet.values()) {
                    currentOrdinal++;
                    int rsOrdinal = row.getInt("ORDINAL_POSITION");
                    if (rsOrdinal != currentOrdinal) {
                        log.fine(
                                LogType.LOG, String.format(
                                        "Repairing ORDINAL_POSITION with gaps for table=%s, column name=%s, " +
                                                "bad ordinal=%d, new ordinal=%d",
                                        relation.getName(),
                                        row.getString("COLUMN_NAME"),
                                        rsOrdinal,
                                        currentOrdinal
                                )
                        );
                        row.set("ORDINAL_POSITION", currentOrdinal);
                    }
                }

                // Iterate through all (repaired) rows and add the columns to our result.
                for (CachedRow row : allColumnsMetadataRs) {
                    Column column = readColumn(row, relation, database);
                    setAutoIncrementDetails(column, database, snapshot);
                    column.setAttribute(LIQUIBASE_COMPLETE, true);
                    relation.getColumns().add(column);
                }
            } catch (SQLException e) {
                throw new DatabaseException(e);
            }
        }

    }

    protected void setAutoIncrementDetails(Column column, Database database, DatabaseSnapshot snapshot) {
        if ((column.getAutoIncrementInformation() != null) && (database instanceof MSSQLDatabase) && (database
            .getConnection() != null) && !(database.getConnection() instanceof OfflineConnection)) {
            Map<String, Column.AutoIncrementInformation> autoIncrementColumns =
                (Map) snapshot.getScratchData("autoIncrementColumns");
            if (autoIncrementColumns == null) {
                autoIncrementColumns = new HashMap<>();
                Executor executor = ExecutorService.getInstance().getExecutor(database);
                try {
                    List<Map<String, ?>> rows = executor.queryForList(
                        new RawSqlStatement(
                            "SELECT object_schema_name(object_id) AS schema_name, " +
                                "object_name(object_id) AS table_name, name AS column_name, " +
                                "CAST(seed_value AS bigint) AS start_value, " +
                                "CAST(increment_value AS bigint) AS increment_by " +
                                "FROM sys.identity_columns"));
                    for (Map row : rows) {
                        String schemaName = (String) row.get("SCHEMA_NAME");
                        String tableName = (String) row.get("TABLE_NAME");
                        String columnName = (String) row.get("COLUMN_NAME");
                        Long startValue = (Long) row.get("START_VALUE");
                        Long incrementBy = (Long) row.get("INCREMENT_BY");

                        Column.AutoIncrementInformation info =
                            new Column.AutoIncrementInformation(startValue, incrementBy);
                        autoIncrementColumns.put(schemaName + "." + tableName + "." + columnName, info);
                    }
                    snapshot.setScratchData("autoIncrementColumns", autoIncrementColumns);
                } catch (DatabaseException e) {
                    Scope.getCurrentScope().getLog(getClass()).info(LogType.LOG, "Could not read identity information", e);
                }
            }
            if ((column.getRelation() != null) && (column.getSchema() != null)) {
                Column.AutoIncrementInformation autoIncrementInformation =
                    autoIncrementColumns.get(column.getSchema().getName() + "." + column.getRelation().getName()
                        + "." + column.getName());
                if (autoIncrementInformation != null) {
                    column.setAutoIncrementInformation(autoIncrementInformation);
                }
            }
        }
    }

    protected Column readColumn(CachedRow columnMetadataResultSet, Relation table, Database database)
        throws SQLException, DatabaseException {
        String rawTableName = (String) columnMetadataResultSet.get("TABLE_NAME");
        String rawColumnName = (String) columnMetadataResultSet.get("COLUMN_NAME");
        String rawSchemaName = StringUtil.trimToNull((String) columnMetadataResultSet.get("TABLE_SCHEM"));
        String rawCatalogName = StringUtil.trimToNull((String) columnMetadataResultSet.get("TABLE_CAT"));
        String remarks = StringUtil.trimToNull((String) columnMetadataResultSet.get("REMARKS"));
        if (remarks != null) {
            // Comes back escaped sometimes
            remarks = remarks.replace("''", "'");
        }
        Integer position = columnMetadataResultSet.getInt("ORDINAL_POSITION");

        Column column = new Column();
        column.setName(StringUtil.trimToNull(rawColumnName));
        column.setRelation(table);
        column.setRemarks(remarks);
        column.setOrder(position);

        if (columnMetadataResultSet.get("IS_FILESTREAM") != null && (Boolean) columnMetadataResultSet.get("IS_FILESTREAM")) {
            column.setAttribute("fileStream", true);
        }
        if (columnMetadataResultSet.get("IS_ROWGUIDCOL") != null && (Boolean) columnMetadataResultSet.get("IS_ROWGUIDCOL")) {
            column.setAttribute("rowGuid", true);
        }
        if (database instanceof OracleDatabase) {
            String nullable = columnMetadataResultSet.getString("NULLABLE");
            if ("Y".equals(nullable)) {
                column.setNullable(true);
            } else {
                column.setNullable(false);
            }
        } else {
            Integer nullable = columnMetadataResultSet.getInt("NULLABLE");
            if (nullable != null) {
                if (nullable == DatabaseMetaData.columnNoNulls) {
                    column.setNullable(false);
                } else if (nullable == DatabaseMetaData.columnNullable) {
                    column.setNullable(true);
                } else if (nullable == DatabaseMetaData.columnNullableUnknown) {
                    Scope.getCurrentScope().getLog(getClass()).info(LogType.LOG, "Unknown nullable state for column "
                            + column.toString() + ". Assuming nullable");
                    column.setNullable(true);
                }
            }
        }

        if (database.supportsAutoIncrement()) {
            if (table instanceof Table) {
                if (database instanceof OracleDatabase) {
                    String data_default = StringUtil.trimToEmpty((String) columnMetadataResultSet.get("DATA_DEFAULT")).toLowerCase();
                    if (data_default.contains("iseq$$") && data_default.endsWith("nextval")) {
                        column.setAutoIncrementInformation(new Column.AutoIncrementInformation());
                    }
                } else {
                    if (columnMetadataResultSet.containsColumn("IS_AUTOINCREMENT")) {
                        String isAutoincrement = (String) columnMetadataResultSet.get("IS_AUTOINCREMENT");
                        isAutoincrement = StringUtil.trimToNull(isAutoincrement);
                        if (isAutoincrement == null) {
                            column.setAutoIncrementInformation(null);
                        } else if (isAutoincrement.equals("YES")) {
                            column.setAutoIncrementInformation(new Column.AutoIncrementInformation());
                        } else if (isAutoincrement.equals("NO")) {
                            column.setAutoIncrementInformation(null);
                        } else if (isAutoincrement.equals("")) {
                            Scope.getCurrentScope().getLog(getClass()).info("Unknown auto increment state for column " + column.toString() + ". Assuming not auto increment");
                            column.setAutoIncrementInformation(null);
                        } else {
                            throw new UnexpectedLiquibaseException("Unknown is_autoincrement value: '" + isAutoincrement + "'");
                        }
                    } else {
                        //probably older version of java, need to select from the column to find out if it is auto-increment
                        String selectStatement;
                        if (database.getDatabaseProductName().startsWith("DB2 UDB for AS/400")) {
                            selectStatement = "select " + database.escapeColumnName(rawCatalogName, rawSchemaName, rawTableName, rawColumnName) + " from " + rawSchemaName + "." + rawTableName + " where 0=1";
                            Scope.getCurrentScope().getLog(getClass()).fine("rawCatalogName : <" + rawCatalogName + ">");
                            Scope.getCurrentScope().getLog(getClass()).fine("rawSchemaName : <" + rawSchemaName + ">");
                            Scope.getCurrentScope().getLog(getClass()).fine("rawTableName : <" + rawTableName + ">");
                            Scope.getCurrentScope().getLog(getClass()).fine("raw selectStatement : <" + selectStatement + ">");

                        } else {
                            selectStatement = "select " + database.escapeColumnName(rawCatalogName, rawSchemaName, rawTableName, rawColumnName) + " from " + database.escapeTableName(rawCatalogName, rawSchemaName, rawTableName) + " where 0=1";
                        }
                        Scope.getCurrentScope().getLog(getClass()).fine("Checking " + rawTableName + "." + rawCatalogName + " for auto-increment with SQL: '" + selectStatement + "'");
                        Connection underlyingConnection = ((JdbcConnection) database.getConnection()).getUnderlyingConnection();
                        Statement statement = null;
                        ResultSet columnSelectRS = null;

                        try {
                            statement = underlyingConnection.createStatement();
                            columnSelectRS = statement.executeQuery(selectStatement);
                            if (columnSelectRS.getMetaData().isAutoIncrement(1)) {
                                column.setAutoIncrementInformation(new Column.AutoIncrementInformation());
                            } else {
                                column.setAutoIncrementInformation(null);
                            }
                        } finally {
                            try {
                                if (statement != null) {
                                    statement.close();
                                }
                            } catch (SQLException ignore) {
                            }
                            if (columnSelectRS != null) {
                                columnSelectRS.close();
                            }
                        }
                    }
                }
            }
        }

        DataType type = readDataType(columnMetadataResultSet, column, database);
        column.setType(type);

        Object defaultValue = readDefaultValue(columnMetadataResultSet, column, database);

        // TODO Is uppercasing the potential function name always a good idea?
        // In theory, we could get a quoted function name (inprobable, but not impossible)
        if ((defaultValue != null) && (defaultValue instanceof DatabaseFunction) && ((DatabaseFunction) defaultValue)
            .getValue().matches("\\w+")) {
            defaultValue = new DatabaseFunction(((DatabaseFunction) defaultValue).getValue().toUpperCase());
        }
        column.setDefaultValue(defaultValue);
        column.setDefaultValueConstraintName(columnMetadataResultSet.getString("COLUMN_DEF_NAME"));

        return column;
    }

    /**
     * Processes metadata of a column, e.g. name, type and default value. We start with the result of the JDBC
     * {@link DatabaseMetaData}.getColumns() method. Depending on Database, additional columns might be present.
     *
     * @param columnMetadataResultSet the result from the JDBC getColumns() call for the column
     * @param column                  logical definition of the column (object form)
     * @param database                the database from which the column originates
     * @return a DataType object with detailed information about the type
     * @throws DatabaseException If an error occurs during processing (mostly caused by Exceptions in JDBC calls)
     */
    protected DataType readDataType(CachedRow columnMetadataResultSet, Column column, Database database) throws DatabaseException {

        if (database instanceof OracleDatabase) {
            String dataType = columnMetadataResultSet.getString("DATA_TYPE_NAME");
            dataType = dataType.replace("VARCHAR2", "VARCHAR");
            dataType = dataType.replace("NVARCHAR2", "NVARCHAR");

            DataType type = new DataType(dataType);
            type.setDataTypeId(columnMetadataResultSet.getInt("DATA_TYPE"));
            if (dataType.equalsIgnoreCase("NUMBER")) {
                type.setColumnSize(columnMetadataResultSet.getInt("DATA_PRECISION"));
//                if (type.getColumnSize() == null) {
//                    type.setColumnSize(38);
//                }
                type.setDecimalDigits(columnMetadataResultSet.getInt("DATA_SCALE"));
//                if (type.getDecimalDigits() == null) {
//                    type.setDecimalDigits(0);
//                }
//            type.setRadix(10);
            } else {
                type.setColumnSize(columnMetadataResultSet.getInt("DATA_LENGTH"));

                boolean isTimeStampDataType = dataType.toUpperCase().contains("TIMESTAMP");

                if (isTimeStampDataType || dataType.equalsIgnoreCase("NCLOB") || dataType.equalsIgnoreCase("BLOB") || dataType.equalsIgnoreCase("CLOB")) {
                    type.setColumnSize(null);
                } else if (dataType.equalsIgnoreCase("NVARCHAR") || dataType.equalsIgnoreCase("NCHAR")) {
                    type.setColumnSize(columnMetadataResultSet.getInt("CHAR_LENGTH"));
                    type.setColumnSizeUnit(DataType.ColumnSizeUnit.CHAR);
                } else {
                    String charUsed = columnMetadataResultSet.getString("CHAR_USED");
                    DataType.ColumnSizeUnit unit = null;
                    if ("C".equals(charUsed)) {
                        unit = DataType.ColumnSizeUnit.CHAR;
                        type.setColumnSize(columnMetadataResultSet.getInt("CHAR_LENGTH"));
                    } else if ("B".equals(charUsed)) {
                        unit = DataType.ColumnSizeUnit.BYTE;
                    }
                    type.setColumnSizeUnit(unit);
                }
            }

            return type;
        }

        String columnTypeName = (String) columnMetadataResultSet.get("TYPE_NAME");

        if (database instanceof MSSQLDatabase) {
            if ("numeric() identity".equalsIgnoreCase(columnTypeName)) {
                columnTypeName = "numeric";
            } else if ("decimal() identity".equalsIgnoreCase(columnTypeName)) {
                columnTypeName = "decimal";
            } else if ("xml".equalsIgnoreCase(columnTypeName)) {
                columnMetadataResultSet.set("COLUMN_SIZE", null);
                columnMetadataResultSet.set("DECIMAL_DIGITS", null);
            } else if ("datetimeoffset".equalsIgnoreCase(columnTypeName)
                || "time".equalsIgnoreCase(columnTypeName)) {
                columnMetadataResultSet.set("COLUMN_SIZE", columnMetadataResultSet.getInt("DECIMAL_DIGITS"));
                columnMetadataResultSet.set("DECIMAL_DIGITS", null);
            }
        }

        if (database instanceof FirebirdDatabase) {
            if ("BLOB SUB_TYPE 0".equals(columnTypeName)) {
                columnTypeName = "BLOB";
            }
            if ("BLOB SUB_TYPE 1".equals(columnTypeName)) {
                columnTypeName = "CLOB";
            }
        }

        if ((database instanceof MySQLDatabase) && ("ENUM".equalsIgnoreCase(columnTypeName) || "SET".equalsIgnoreCase
            (columnTypeName))) {
            try {
                String boilerLength;
                if ("ENUM".equalsIgnoreCase(columnTypeName)) {
                    boilerLength = "7";
                } else  {
                    // SET
                    boilerLength = "6";
                }
                List<String> enumValues = ExecutorService.getInstance().getExecutor(database).queryForList(
                    new RawSqlStatement(
                        "SELECT DISTINCT SUBSTRING_INDEX(SUBSTRING_INDEX(SUBSTRING(COLUMN_TYPE, " + boilerLength +
                            ", LENGTH(COLUMN_TYPE) - " + boilerLength +
                            " - 1 ), \"','\", 1 + units.i + tens.i * 10) , \"','\", -1)\n" +
                            "FROM INFORMATION_SCHEMA.COLUMNS\n" +
                            "CROSS JOIN (SELECT 0 AS i UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 " +
                            "UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) units\n" +
                            "CROSS JOIN (SELECT 0 AS i UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 " +
                            "UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) tens\n" +
                            "WHERE TABLE_NAME = '" + column.getRelation().getName() + "' \n" +
                            "AND COLUMN_NAME = '" + column.getName() + "'"), String.class);
                String enumClause = "";
                for (String enumValue : enumValues) {
                    enumClause += "'" + enumValue + "', ";
                }
                enumClause = enumClause.replaceFirst(", $", "");
                return new DataType(columnTypeName + "(" + enumClause + ")");
            } catch (DatabaseException e) {
                Scope.getCurrentScope().getLog(getClass()).warning(LogType.LOG, "Error fetching enum values", e);
            }
        }

        DataType.ColumnSizeUnit columnSizeUnit = DataType.ColumnSizeUnit.BYTE;

        int dataType = columnMetadataResultSet.getInt("DATA_TYPE");
        Integer columnSize = null;
        Integer decimalDigits = null;

        if (!database.dataTypeIsNotModifiable(columnTypeName)) {
            // don't set size for types like int4, int8 etc
            columnSize = columnMetadataResultSet.getInt("COLUMN_SIZE");
            decimalDigits = columnMetadataResultSet.getInt("DECIMAL_DIGITS");
            if ((decimalDigits != null) && decimalDigits.equals(0)) {
                decimalDigits = null;
            }
        }

        Integer radix = columnMetadataResultSet.getInt("NUM_PREC_RADIX");

        Integer characterOctetLength = columnMetadataResultSet.getInt("CHAR_OCTET_LENGTH");

        if (database instanceof AbstractDb2Database) {
            String typeName = columnMetadataResultSet.getString("TYPE_NAME");
            if (("DBCLOB".equalsIgnoreCase(typeName) || "GRAPHIC".equalsIgnoreCase(typeName)
                || "VARGRAPHIC".equalsIgnoreCase(typeName)) &&(columnSize != null)) {
                //Stored as double length chars
                columnSize = columnSize / 2;
            }
            if ("TIMESTAMP".equalsIgnoreCase(columnTypeName) && (decimalDigits == null)) {
                // Actually a date
                columnTypeName = "DATE";
                dataType = Types.DATE;
            }
        }

        if ((database instanceof PostgresDatabase) && (columnSize != null) && columnSize.equals(Integer.MAX_VALUE)) {
            columnSize = null;
        }

        // For SAP (Sybase) SQL ANywhere, JDBC returns "LONG(2147483647) binary" (the number is 2^31-1)
        // but when creating a column, LONG BINARY must not have parameters.
        // The same applies to LONG(...) VARCHAR.
        if (database instanceof SybaseASADatabase
            && ("LONG BINARY".equalsIgnoreCase(columnTypeName) || "LONG VARCHAR".equalsIgnoreCase(columnTypeName))) {
            columnSize = null;
        }

        DataType type = new DataType(columnTypeName);
        type.setDataTypeId(dataType);

        /*
         * According to the description of DatabaseMetaData.getColumns, the content of the "COLUMN_SIZE" column is
         * pretty worthless for datetime/timestamp columns:
         *
         * "For datetime datatypes, this is the length in characters of the String representation
         * (assuming the maximum allowed precision of the fractional seconds component)."
         * In the case of TIMESTAMP columns, the information we are really looking for
         * (the fractional digits) is located in the column DECIMAL_DIGITS.
         */
        int jdbcType = columnMetadataResultSet.getInt("DATA_TYPE");

        // Java 8 compatibility notes: When upgrading this project to JDK8 and beyond, also execute this if-branch
        // if jdbcType is TIMESTAMP_WITH_TIMEZONE (does not exist yet in JDK7)
        if (jdbcType == Types.TIMESTAMP) {

            if (decimalDigits == null) {
                type.setColumnSize(null);
            } else {
                type.setColumnSize((decimalDigits != database.getDefaultFractionalDigitsForTimestamp()) ?
                    decimalDigits : null
                );
            }

            type.setDecimalDigits(null);
        } else {
            type.setColumnSize(columnSize);
            type.setDecimalDigits(decimalDigits);
        }
        type.setRadix(radix);
        type.setCharacterOctetLength(characterOctetLength);
        type.setColumnSizeUnit(columnSizeUnit);

        return type;
    }

    protected Object readDefaultValue(CachedRow columnMetadataResultSet, Column columnInfo, Database database) {
        if (database instanceof MSSQLDatabase) {
            Object defaultValue = columnMetadataResultSet.get(COLUMN_DEF_COL);

            if (((defaultValue != null) && (defaultValue instanceof String)) && ("(NULL)".equals(defaultValue))) {
                columnMetadataResultSet.set(COLUMN_DEF_COL, new DatabaseFunction("null"));
            }
        }

        if ((database instanceof OracleDatabase) && (columnMetadataResultSet.get(COLUMN_DEF_COL) == null)) {
            columnMetadataResultSet.set(COLUMN_DEF_COL, columnMetadataResultSet.get("DATA_DEFAULT"));

            if ((columnMetadataResultSet.get(COLUMN_DEF_COL) != null) && "NULL".equalsIgnoreCase((String)
                columnMetadataResultSet.get(COLUMN_DEF_COL))) {
                columnMetadataResultSet.set(COLUMN_DEF_COL, null);
            }

            Object columnDef = columnMetadataResultSet.get(COLUMN_DEF_COL);
            if ("CHAR".equalsIgnoreCase(columnInfo.getType().getTypeName()) && (columnDef instanceof String) && !
                ((String) columnDef).startsWith("'") && !((String) columnDef).endsWith("'")) {
                return new DatabaseFunction((String) columnDef);
            }

            if ("YES".equals(columnMetadataResultSet.get("VIRTUAL_COLUMN"))) {
                Object virtColumnDef = columnMetadataResultSet.get(COLUMN_DEF_COL);
                if ((virtColumnDef != null) && !"null".equals(virtColumnDef)) {
                    columnMetadataResultSet.set(COLUMN_DEF_COL, "GENERATED ALWAYS AS (" + virtColumnDef + ")");
                }
            }

            Object defaultValue = columnMetadataResultSet.get(COLUMN_DEF_COL);
            if ((defaultValue != null) && (defaultValue instanceof String)) {
                String lowerCaseDefaultValue = ((String) defaultValue).toLowerCase();
                if (lowerCaseDefaultValue.contains("iseq$$") && lowerCaseDefaultValue.endsWith(".nextval")) {
                    columnMetadataResultSet.set(COLUMN_DEF_COL, null);
                }

            }
        }

        if (database instanceof PostgresDatabase) {
            Object defaultValue = columnMetadataResultSet.get(COLUMN_DEF_COL);
            if ((defaultValue != null) && (defaultValue instanceof String)) {
                Matcher matcher = postgresStringValuePattern.matcher((String) defaultValue);
                if (matcher.matches()) {
                    defaultValue = matcher.group(1);
                } else {
                    matcher = postgresNumberValuePattern.matcher((String) defaultValue);
                    if (matcher.matches()) {
                        defaultValue = matcher.group(1);
                    }

                }
                columnMetadataResultSet.set(COLUMN_DEF_COL, defaultValue);
            }
        }

        if (
            (database instanceof AbstractDb2Database) &&
                ((columnMetadataResultSet.get(COLUMN_DEF_COL) != null) &&
                    "NULL".equalsIgnoreCase((String) columnMetadataResultSet.get(COLUMN_DEF_COL)))) {
            columnMetadataResultSet.set(COLUMN_DEF_COL, null);
        }

        return SqlUtil.parseValue(database, columnMetadataResultSet.get(COLUMN_DEF_COL), columnInfo.getType());
    }

}


package liquibase.snapshot.jvm;

import liquibase.database.Database;
import liquibase.database.core.*;
import liquibase.exception.DatabaseException;
import liquibase.exception.UnexpectedLiquibaseException;
import liquibase.executor.ExecutorService;
import liquibase.snapshot.CachedRow;
import liquibase.snapshot.DatabaseSnapshot;
import liquibase.snapshot.InvalidExampleException;
import liquibase.snapshot.JdbcDatabaseSnapshot;
import liquibase.statement.core.RawSqlStatement;
import liquibase.structure.DatabaseObject;
import liquibase.structure.core.*;
import liquibase.util.StringUtil;

import java.sql.SQLException;
import java.util.*;

public class UniqueConstraintSnapshotGenerator extends JdbcSnapshotGenerator {

    public UniqueConstraintSnapshotGenerator() {
        super(UniqueConstraint.class, new Class[]{Table.class});
    }

    @Override
    public int getPriority(Class<? extends DatabaseObject> objectType, Database database) {
        if (database instanceof SQLiteDatabase) {
            return PRIORITY_NONE;
        }
        return super.getPriority(objectType, database);
    }

    @Override
    protected DatabaseObject snapshotObject(DatabaseObject example, DatabaseSnapshot snapshot) throws DatabaseException, InvalidExampleException {
        Database database = snapshot.getDatabase();
        UniqueConstraint exampleConstraint = (UniqueConstraint) example;
        Relation table = exampleConstraint.getRelation();

        List<Map<String, ?>> metadata = listColumns(exampleConstraint, database, snapshot);

        if (metadata.isEmpty()) {
            return null;
        }
        UniqueConstraint constraint = new UniqueConstraint();
        constraint.setRelation(table);
        constraint.setName(example.getName());
        constraint.setBackingIndex(exampleConstraint.getBackingIndex());
        constraint.setInitiallyDeferred(((UniqueConstraint) example).isInitiallyDeferred());
        constraint.setDeferrable(((UniqueConstraint) example).isDeferrable());
        constraint.setClustered(((UniqueConstraint) example).isClustered());

        for (Map<String, ?> col : metadata) {
            String ascOrDesc = (String) col.get("ASC_OR_DESC");
            Boolean descending = "D".equals(ascOrDesc) ? Boolean.TRUE : ("A".equals(ascOrDesc) ? Boolean.FALSE : null);
            if (database instanceof H2Database) {
                for (String columnName : StringUtil.splitAndTrim((String) col.get("COLUMN_NAME"), ",")) {
                    constraint.getColumns().add(new Column(columnName).setDescending(descending).setRelation(table));
                }
            } else {
                constraint.getColumns().add(new Column((String) col.get("COLUMN_NAME")).setDescending(descending).setRelation(table));
            }
            setValidateOptionIfAvailable(database, constraint, col);
        }

        return constraint;
    }

    /**
     * Method to map 'validate' option for UC. This thing works only for ORACLE
     *
     * @param database - DB where UC will be created
     * @param uniqueConstraint - UC object to persist validate option
     * @param columnsMetadata - it's a cache-map to get metadata about UC
     */
    private void setValidateOptionIfAvailable(Database database, UniqueConstraint uniqueConstraint, Map<String, ?> columnsMetadata) {
        if (!(database instanceof OracleDatabase)) {
            return;
        }
        final Object constraintValidate = columnsMetadata.get("CONSTRAINT_VALIDATE");
        final String VALIDATE = "VALIDATED";
        if (constraintValidate!=null && !constraintValidate.toString().trim().isEmpty()) {
            uniqueConstraint.setShouldValidate(VALIDATE.equals(cleanNameFromDatabase(constraintValidate.toString().trim(), database)));
        }
    }

    @Override
    protected void addTo(DatabaseObject foundObject, DatabaseSnapshot snapshot) throws DatabaseException, InvalidExampleException {

        if (!snapshot.getSnapshotControl().shouldInclude(UniqueConstraint.class)) {
            return;
        }

        if (foundObject instanceof Table) {
            Table table = (Table) foundObject;
            Database database = snapshot.getDatabase();
            Schema schema;
            schema = table.getSchema();

            List<CachedRow> metadata = null;
            try {
                metadata = listConstraints(table, snapshot, schema);
            } catch (SQLException e) {
                throw new DatabaseException(e);
            }

            Set<String> seenConstraints = new HashSet<>();

            for (CachedRow constraint : metadata) {
                UniqueConstraint uq = new UniqueConstraint()
                        .setName(cleanNameFromDatabase((String) constraint.get("CONSTRAINT_NAME"), database)).setRelation(table);
                if (constraint.containsColumn("INDEX_NAME")) {
                    uq.setBackingIndex(new Index((String) constraint.get("INDEX_NAME"), (String) constraint.get("INDEX_CATALOG"), (String) constraint.get("INDEX_SCHEMA"), table.getName()));
                }
                if ("CLUSTERED".equals(constraint.get("TYPE_DESC"))) {
                    uq.setClustered(true);
                }
                if (seenConstraints.add(uq.getName())) {
                    table.getUniqueConstraints().add(uq);
                }
            }
        }
    }

    protected List<CachedRow> listConstraints(Table table, DatabaseSnapshot snapshot, Schema schema) throws DatabaseException, SQLException {
        return ((JdbcDatabaseSnapshot) snapshot).getMetaDataFromCache().getUniqueConstraints(schema.getCatalogName(), schema.getName(), table.getName());
    }

    protected List<Map<String, ?>> listColumns(UniqueConstraint example, Database database, DatabaseSnapshot snapshot) throws DatabaseException {
        Relation table = example.getRelation();
        Schema schema = table.getSchema();
        String name = example.getName();

        boolean bulkQuery;
        String sql;

        String cacheKey = "uniqueConstraints-"+example.getClass().getSimpleName()+"-"+example.getSchema().toCatalogAndSchema().customize(database).toString();
        String queryCountKey = "uniqueConstraints-"+example.getClass().getSimpleName()+"-queryCount";

        Map<String, List<Map<String, ?>>> columnCache = (Map<String, List<Map<String, ?>>>) snapshot.getScratchData(cacheKey);
        Integer columnQueryCount = (Integer) snapshot.getScratchData(queryCountKey);
        if (columnQueryCount == null) {
            columnQueryCount = 0;
        }

        if (columnCache == null) {
            bulkQuery = ((database instanceof OracleDatabase) || (database instanceof MSSQLDatabase)) &&
                (columnQueryCount > 3);
            snapshot.setScratchData(queryCountKey, columnQueryCount + 1);

            if ((database instanceof MySQLDatabase) || (database instanceof HsqlDatabase)) {
                sql = "select const.CONSTRAINT_NAME, COLUMN_NAME "
                        + "from " + database.getSystemSchema() + ".table_constraints const "
                        + "join " + database.getSystemSchema() + ".key_column_usage col "
                        + "on const.constraint_schema=col.constraint_schema "
                        + "and const.table_name=col.table_name "
                        + "and const.constraint_name=col.constraint_name "
                        + "where const.constraint_schema='" + database.correctObjectName(schema.getCatalogName(), Catalog.class) + "' "
                        + "and const.table_name='" + database.correctObjectName(example.getRelation().getName(), Table.class) + "' "
                        + "and const.constraint_name='" + database.correctObjectName(name, UniqueConstraint.class) + "'"
                        + "order by ordinal_position";
            } else if (database instanceof PostgresDatabase) {
                sql = "select const.CONSTRAINT_NAME, COLUMN_NAME "
                        + "from " + database.getSystemSchema() + ".table_constraints const "
                        + "join " + database.getSystemSchema() + ".key_column_usage col "
                        + "on const.constraint_schema=col.constraint_schema "
                        + "and const.table_name=col.table_name "
                        + "and const.constraint_name=col.constraint_name "
                        + "where const.constraint_catalog='" + database.correctObjectName(schema.getCatalogName(), Catalog.class) + "' "
                        + "and const.constraint_schema='" + database.correctObjectName(schema.getSchema().getName(), Schema.class) + "' "
                        + "and const.table_name='" + database.correctObjectName(example.getRelation().getName(), Table.class) + "' "
                        + "and const.constraint_name='" + database.correctObjectName(name, UniqueConstraint.class) + "'"
                        + "order by ordinal_position";
            } else if (database instanceof MSSQLDatabase) {
                sql =
                        "SELECT " +
                                "[kc].[name] AS [CONSTRAINT_NAME], " +
                                    "s.name AS constraint_container, "+
                                "[c].[name] AS [COLUMN_NAME], " +
                                "CASE [ic].[is_descending_key] WHEN 0 THEN N'A' WHEN 1 THEN N'D' END AS [ASC_OR_DESC] " +
                                "FROM [sys].[schemas] AS [s] " +
                                "INNER JOIN [sys].[tables] AS [t] " +
                                "ON [t].[schema_id] = [s].[schema_id] " +
                                "INNER JOIN [sys].[key_constraints] AS [kc] " +
                                "ON [kc].[parent_object_id] = [t].[object_id] " +
                                "INNER JOIN [sys].[indexes] AS [i] " +
                                "ON [i].[object_id] = [kc].[parent_object_id] " +
                                "AND [i].[index_id] = [kc].[unique_index_id] " +
                                "INNER JOIN [sys].[index_columns] AS [ic] " +
                                "ON [ic].[object_id] = [i].[object_id] " +
                                "AND [ic].[index_id] = [i].[index_id] " +
                                "INNER JOIN [sys].[columns] AS [c] " +
                                "ON [c].[object_id] = [ic].[object_id] " +
                                "AND [c].[column_id] = [ic].[column_id] " +
                                    "WHERE [s].[name] = N'" + database.escapeStringForDatabase(database.correctObjectName(schema.getName(), Schema.class)) + "' ";
                    if (!bulkQuery) {
                        sql += "AND [t].[name] = N'" + database.escapeStringForDatabase(database.correctObjectName(example.getRelation().getName(), Table.class)) + "' " +
                                "AND [kc].[name] = N'" + database.escapeStringForDatabase(database.correctObjectName(name, UniqueConstraint.class)) + "' ";
                    }
                    sql += "ORDER BY " +
                            "[ic].[key_ordinal]";
            } else if (database instanceof OracleDatabase) {
                sql = "select ucc.owner as constraint_container, ucc.constraint_name as constraint_name, ucc.column_name, f.validated as constraint_validate " +
                        "from all_cons_columns ucc " +
                        "INNER JOIN all_constraints f " +
                        "ON ucc.owner = f.owner " +
                        "AND ucc.constraint_name = f.constraint_name " +
                        "where " +
                        (bulkQuery ? "" : "ucc.constraint_name='" + database.correctObjectName(name, UniqueConstraint.class) + "' and ") +
                        "ucc.owner='" + database.correctObjectName(schema.getCatalogName(), Catalog.class) + "' " +
                        "and ucc.table_name not like 'BIN$%' "+
                        "order by ucc.position";
            } else if (database instanceof DB2Database) {
                if (database.getDatabaseProductName().startsWith("DB2 UDB for AS/400")) {
                    sql = "select T1.constraint_name as CONSTRAINT_NAME, T2.COLUMN_NAME as COLUMN_NAME from QSYS2.TABLE_CONSTRAINTS T1, QSYS2.SYSCSTCOL T2\n"
                            + "where T1.CONSTRAINT_TYPE='UNIQUE' and T1.CONSTRAINT_NAME=T2.CONSTRAINT_NAME\n"
                            + "and T1.CONSTRAINT_SCHEMA='" + database.correctObjectName(schema.getName(), Schema.class) + "'\n"
                            + "and T2.CONSTRAINT_SCHEMA='" + database.correctObjectName(schema.getName(), Schema.class) + "'\n"
                            //+ "T2.TABLE_NAME='"+ database.correctObjectName(example.getTable().getName(), Table.class) + "'\n"
                            //+ "\n"
                            + "order by T2.COLUMN_NAME\n";

                } else {
                    sql = "select k.colname as column_name from syscat.keycoluse k, syscat.tabconst t "
                            + "where k.constname = t.constname "
                            + "and k.tabschema = t.tabschema "
                            + "and t.type='U' "
                            + "and k.constname='" + database.correctObjectName(name, UniqueConstraint.class) + "' "
                            + "and t.tabschema = '" + database.correctObjectName(schema.getName(), Schema.class) + "' "
                            + "order by colseq";
                }
            } else if (database instanceof DerbyDatabase) {
                sql = "SELECT cg.descriptor as descriptor, t.tablename "
                        + "FROM sys.sysconglomerates cg "
                        + "JOIN sys.syskeys k ON cg.conglomerateid = k.conglomerateid "
                        + "JOIN sys.sysconstraints c ON c.constraintid = k.constraintid "
                        + "JOIN sys.systables t ON c.tableid = t.tableid "
                        + "WHERE c.constraintname='" + database.correctObjectName(name, UniqueConstraint.class) + "'";
                List<Map<String, ?>> rows = ExecutorService.getInstance().getExecutor(database).queryForList(new RawSqlStatement(sql));

                List<Map<String, ?>> returnList = new ArrayList<>();
                if (rows.isEmpty()) {
                    return returnList;
                } else if (rows.size() > 1) {
                    throw new UnexpectedLiquibaseException("Got multiple rows back querying unique constraints");
                } else {
                    Map rowData = rows.get(0);
                    String descriptor = rowData.get("DESCRIPTOR").toString();
                    descriptor = descriptor.replaceFirst(".*\\(", "").replaceFirst("\\).*", "");
                    for (String columnNumber : StringUtil.splitAndTrim(descriptor, ",")) {
                        String columnName = (String) ExecutorService.getInstance().getExecutor(database).queryForObject(new RawSqlStatement(
                                "select c.columnname from sys.syscolumns c "
                                        + "join sys.systables t on t.tableid=c.referenceid "
                                        + "where t.tablename='" + rowData.get("TABLENAME") + "' and c.columnnumber=" + columnNumber), String.class);

                        Map<String, String> row = new HashMap<>();
                        row.put("COLUMN_NAME", columnName);
                        returnList.add(row);
                    }
                    return returnList;
                }

            } else if (database instanceof FirebirdDatabase) {
                // Careful! FIELD_NAME and INDEX_NAME in RDB$INDEX_SEGMENTS are CHAR, not VARCHAR columns.
                sql = "SELECT TRIM(RDB$INDEX_SEGMENTS.RDB$FIELD_NAME) AS column_name " +
                        "FROM RDB$INDEX_SEGMENTS " +
                        "LEFT JOIN RDB$INDICES ON RDB$INDICES.RDB$INDEX_NAME = RDB$INDEX_SEGMENTS.RDB$INDEX_NAME " +
                        "WHERE UPPER(TRIM(RDB$INDICES.RDB$INDEX_NAME))='" + database.correctObjectName(name, UniqueConstraint.class) + "' " +
                        "ORDER BY RDB$INDEX_SEGMENTS.RDB$FIELD_POSITION";
            } else if (database instanceof SybaseASADatabase) {
                sql = "select sysconstraint.constraint_name, syscolumn.column_name " +
                        "from sysconstraint, syscolumn, systable " +
                        "where sysconstraint.ref_object_id = syscolumn.object_id " +
                        "and sysconstraint.table_object_id = systable.object_id " +
                        "and sysconstraint.constraint_name = '" + database.correctObjectName(name, UniqueConstraint.class) + "' " +
                        "and systable.table_name = '" + database.correctObjectName(example.getRelation().getName(), Table.class) + "'";
            } else if (database instanceof InformixDatabase) {
                sql = getUniqueConstraintsSqlInformix((InformixDatabase)database, schema, name);
            } else if(database instanceof Ingres9Database) {
                sql = "select constraint_name, column_name " +
                        "from iikeys " +
                        "where constraint_name = '" + database.correctObjectName(name, UniqueConstraint.class) + "' " +
                        "and table_name = '" + database.correctObjectName(example.getTable().getName(), Table.class) + "'";
            } else {
                // If we do not have a specific handler for the RDBMS, we assume that the database has an
                // INFORMATION_SCHEMA we can use. This is a last-resort measure and might fail.
                String catalogName = database.correctObjectName(schema.getCatalogName(), Catalog.class);
                String schemaName = database.correctObjectName(schema.getName(), Schema.class);
                String constraintName = database.correctObjectName(name, UniqueConstraint.class);
                String tableName = database.correctObjectName(table.getName(), Table.class);
                sql = "select CONSTRAINT_NAME, COLUMN_LIST as COLUMN_NAME "
                        + "from " + database.getSystemSchema() + ".constraints "
                        + "where constraint_type='UNIQUE' ";
                if (catalogName != null) {
                    sql += "and constraint_catalog='" + catalogName + "' ";
                }
                if (schemaName != null) {
                    sql += "and constraint_schema='" + schemaName + "' ";
                }
                if (tableName != null) {
                    sql += "and table_name='" + tableName + "' ";
                }
                if (constraintName != null) {
                    sql += "and constraint_name='" + constraintName + "'";
                }
            }
            List<Map<String, ?>> rows = ExecutorService.getInstance().getExecutor(database).queryForList(new RawSqlStatement(sql));

            if (bulkQuery) {
                columnCache = new HashMap<>();
                snapshot.setScratchData(cacheKey, columnCache);
                for (Map<String, ?> row : rows) {
                    String key = row.get("CONSTRAINT_CONTAINER") + "_" + row.get("CONSTRAINT_NAME");
                    List<Map<String, ?>> constraintRows = columnCache.get(key);
                    if (constraintRows == null) {
                        constraintRows = new ArrayList<>();
                        columnCache.put(key, constraintRows);
                    }
                    constraintRows.add(row);
                }

                return listColumns(example, database, snapshot);
            } else {
                return rows;
            }
        } else {
            String lookupKey = schema.getName() + "_" + example.getName();
            List<Map<String, ?>> rows = columnCache.get(lookupKey);
            if (rows == null) {
                rows = new ArrayList<>();
            }
            return rows;
        }

    }

    /**
     * Gets an SQL query that returns the constraint names and columns for all UNIQUE constraints.
     * @param database A database object of the InformixDatabase type
     * @param schema Name of the schema to examine (or null for all)
     * @param name Name of the constraint to examine (or null for all)
     * @return A lengthy SQL statement that fetches the constraint names and columns
     */
    private String getUniqueConstraintsSqlInformix(InformixDatabase database, Schema schema, String name) {
        StringBuffer sqlBuf = new StringBuffer();

        sqlBuf.append("SELECT * FROM (\n");

        // Yes, I am serious about this. It appears there are neither CTE/WITH clauses nor PIVOT/UNPIVOT operators
        // in Informix SQL.
        for (int i=1; i<=16; i++) {
            if (i>1)
                sqlBuf.append("UNION ALL\n");
            sqlBuf.append(
                String.format("  SELECT\n" +
                        "    CONS.owner,\n" +
                        "    CONS.constrname AS constraint_name,\n" +
                        "    COL.colname AS column_name,\n" +
                        "    CONS.constrtype,\n" +
                        "    %d               AS column_index\n" +
                        "  FROM informix.sysconstraints CONS\n" +
                        "    JOIN informix.sysindexes IDX ON CONS.idxname = IDX.idxname\n" +
                        "    JOIN informix.syscolumns COL ON COL.tabid = CONS.tabid AND COL.colno = IDX.part%d\n",
                        i, i
                )
            );
        }

        // Finish the subquery and filter on the U(NIQUE) constraint type
        sqlBuf.append(
                "                ) SUBQ\n" +
                "WHERE constrtype='U' \n");

        String catalogName = database.correctObjectName(schema.getCatalogName(), Catalog.class);
        String constraintName = database.correctObjectName(name, UniqueConstraint.class);

        // If possible, filter for catalog name and/or constraint name
        if (catalogName != null) {
            sqlBuf.append("AND owner='" + catalogName + "'\n");
        }
        if (constraintName != null) {
            sqlBuf.append("AND constraint_name='" + constraintName + "'");
        }

        // For correct processing, it is important that we get all columns in order.
        sqlBuf.append("ORDER BY owner, constraint_name, column_index");

        // Return the query
        return sqlBuf.toString();
    }

}


package liquibase.change;

import liquibase.database.Database;
import liquibase.servicelocator.PrioritizedService;
import liquibase.structure.DatabaseObject;

import java.util.*;

/**
 * Static metadata about a {@code link Change}.
 * This data is kept in a separate class to better separate the information about the change and the fields of the change.
 * It also ensures there will be no conflict between Change fields and metadata, such as "name".
 * ChangeMetaData instances are immutable.
 *
 * @see ChangeParameterMetaData
 */
public class ChangeMetaData implements PrioritizedService {
    public static final int PRIORITY_DEFAULT = 1;

    private String name;
    private String description;
    private int priority;

    private Map<String, ChangeParameterMetaData> parameters;
    private Set<String> appliesTo;
    private HashMap<String, String> databaseNotes = new HashMap<>();

    public ChangeMetaData(String name, String description, int priority, String[] appliesTo, Map<String, String> databaseNotes, Set<ChangeParameterMetaData> parameters) {
        if (parameters == null) {
            parameters  = new HashSet<>();
        }
        if ((appliesTo != null) && (appliesTo.length == 0)) {
            appliesTo  = null;
        }
        this.name = name;
        this.description = description;
        this.priority = priority;

        this.parameters = new HashMap<>();
        for (ChangeParameterMetaData param : parameters) {
            this.parameters.put(param.getParameterName(), param);
        }
        this.parameters = Collections.unmodifiableMap(this.parameters);

        this.appliesTo = null;
        if ((appliesTo != null) && (appliesTo.length > 0)) {
            this.appliesTo = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(appliesTo)));
        }

        if (databaseNotes != null) {
            this.databaseNotes.putAll(databaseNotes);
        }
    }

    /**
     * Return the name of the change used to identify it. The name must not contain spaces since it will be used as tag names in XML etc.
     */
    public String getName() {
        return name;
    }

    /**
     * A description of the Change for documentation purposes.
     */
    public String getDescription() {
        return description;
    }

    /**
     * The priority of a Change implementation controls which implementation is used if several exist for the same "name".
     */
    @Override
    public int getPriority() {
        return priority;
    }

    public String getNotes(String database) {
        return databaseNotes.get(database);
    }

    /**
     * Return the parameters of this Change. Will never return a null map, only an empty or populated map.
     */
    public Map<String, ChangeParameterMetaData> getParameters() {
        return Collections.unmodifiableMap(parameters);
    }

    /**
     * Return the parameters of the given change that are set to a non-null value. Will never return a null map, only an empty or populated map.
     */
    public Map<String, ChangeParameterMetaData> getSetParameters(Change change) {
        Map<String, ChangeParameterMetaData> returnMap = new HashMap<>();
        for (Map.Entry<String, ChangeParameterMetaData> entry : getParameters().entrySet()) {
            if (entry.getValue().getCurrentValue(change) != null) {
                returnMap.put(entry.getKey(), entry.getValue());
            }
        }
        return Collections.unmodifiableMap(returnMap);
    }

    /**
     *  Returns the required parameters for this change for the given database. Will never return a null map, only an empty or populated map.
     */
    public Map<String, ChangeParameterMetaData> getRequiredParameters(Database database) {
        Map<String, ChangeParameterMetaData> returnMap = new HashMap<>();

        for (ChangeParameterMetaData metaData : parameters.values()) {
            if (metaData.isRequiredFor(database)) {
                returnMap.put(metaData.getParameterName(), metaData);
            }
        }
        return returnMap;
    }

    /**
     *  Returns the optional parameters for this change for the given database. Will never return a null map, only an empty or populated map.
     */
    public Map<String, ChangeParameterMetaData> getOptionalParameters(Database database) {
        Map<String, ChangeParameterMetaData> returnMap = new HashMap<>();

        for (ChangeParameterMetaData metaData : parameters.values()) {
            if (!metaData.isRequiredFor(database)) {
                returnMap.put(metaData.getParameterName(), metaData);
            }
        }
        return returnMap;
    }

    /**
     * Returns the types of DatabaseObjects this change would apply to.
     * Useful for documentation or integrations that present a user with what change commands are available for a given database type.
     * If no information is known, returns null. Will never return an empty set, only null or a populated set.
     * The string value will correspond to the values returned by {@link liquibase.structure.DatabaseObject#getObjectTypeName()}
     */
    public Set<String> getAppliesTo() {
        return appliesTo;
    }

    public boolean appliesTo(DatabaseObject databaseObject) {
        return (appliesTo != null) && appliesTo.contains(databaseObject.getObjectTypeName());
    }
}