OpenConcerto

Dépôt officiel du code source de l'ERP OpenConcerto
sonarqube

svn://code.openconcerto.org/openconcerto

Rev

Rev 144 | Blame | Compare with Previous | Last modification | View Log | RSS feed

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright 2011-2019 OpenConcerto, by ILM Informatique. All rights reserved.
 * 
 * The contents of this file are subject to the terms of the GNU General Public License Version 3
 * only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
 * copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
 * language governing permissions and limitations under the License.
 * 
 * When distributing the software, include this License Header Notice in each file.
 */
 
 package org.openconcerto.sql.utils;

import static java.util.Collections.singletonList;

import org.openconcerto.sql.Log;
import org.openconcerto.sql.model.SQLBase;
import org.openconcerto.sql.model.SQLField;
import org.openconcerto.sql.model.SQLName;
import org.openconcerto.sql.model.SQLSyntax;
import org.openconcerto.sql.model.SQLSystem;
import org.openconcerto.sql.model.SQLTable;
import org.openconcerto.sql.model.SQLTable.Index;
import org.openconcerto.sql.model.SQLTable.SQLIndex;
import org.openconcerto.sql.model.SQLType;
import org.openconcerto.sql.model.Where;
import org.openconcerto.sql.model.graph.Link;
import org.openconcerto.sql.model.graph.Link.Rule;
import org.openconcerto.sql.model.graph.SQLKey;
import org.openconcerto.utils.CollectionUtils;
import org.openconcerto.utils.ListMap;
import org.openconcerto.utils.ReflectUtils;
import org.openconcerto.utils.StringUtils;
import org.openconcerto.utils.cc.ITransformer;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Pattern;

import org.h2.api.Trigger;

/**
 * Construct a statement about a table.
 * 
 * @author Sylvain
 * @param <T> type of this
 * @see AlterTable
 * @see SQLCreateTable
 */
public abstract class ChangeTable<T extends ChangeTable<T>> {

    private static final String TRIGGER_SUFFIX = "_trigger";
    protected static final String[] TRIGGER_EVENTS = { "INSERT", "UPDATE" };

    // group 1 is the columns, group 2 the where
    public static final Pattern H2_UNIQUE_TRIGGER_PATTERN = Pattern.compile("\\snew " + PartialUniqueTrigger.class.getName() + "\\(\\s*java.util.Arrays.asList\\((.+)\\)\\s*,(.+)\\)");
    // group 1 is the class name, e.g. CALL
    // "org.openconcerto.sql.utils.ChangeTableTest$Test_PartialUniqueTrigger"
    public static final String H2_UNIQUE_TRIGGER_CLASS_SUFFIX = "_" + PartialUniqueTrigger.class.getSimpleName();
    public static final Pattern H2_UNIQUE_TRIGGER_CLASS_PATTERN = Pattern.compile("CALL\\s+\"(.*" + Pattern.quote(H2_UNIQUE_TRIGGER_CLASS_SUFFIX) + ")\"");
    public static final Pattern H2_LIST_PATTERN = Pattern.compile("\\s*,\\s*");

    public static final String MYSQL_TRIGGER_SUFFIX_1 = getTriggerSuffix(TRIGGER_EVENTS[0]);
    public static final String MYSQL_TRIGGER_SUFFIX_2 = getTriggerSuffix(TRIGGER_EVENTS[1]);
    public static final String MYSQL_FAKE_PROCEDURE = "Unique constraint violation";
    public static final String MYSQL_TRIGGER_EXCEPTION = "call " + SQLBase.quoteIdentifier(MYSQL_FAKE_PROCEDURE);
    // group 1 is the table name, group 2 the where
    public static final Pattern MYSQL_UNIQUE_TRIGGER_PATTERN = Pattern.compile(
            "IF\\s*\\(\\s*" + Pattern.quote("SELECT COUNT(*)") + "\\s+FROM\\s+(.+)\\s+where\\s+(.+)\\)\\s*>\\s*1\\s+then\\s+" + Pattern.quote(MYSQL_TRIGGER_EXCEPTION), Pattern.CASE_INSENSITIVE);
    // to split the where
    public static final Pattern MYSQL_WHERE_PATTERN = Pattern.compile("\\s+and\\s+", Pattern.CASE_INSENSITIVE);
    // to find the column name
    public static final Pattern MYSQL_WHERE_EQ_PATTERN = Pattern.compile("(NEW.)?(.+)\\s*=\\s*(NEW.)?\\2");

    public static final String getIndexName(final String triggerName, final SQLSystem system) {
        if (system == SQLSystem.MYSQL && triggerName.endsWith(MYSQL_TRIGGER_SUFFIX_1)) {
            return triggerName.substring(0, triggerName.length() - MYSQL_TRIGGER_SUFFIX_1.length());
        } else if (system == SQLSystem.H2 && triggerName.endsWith(TRIGGER_SUFFIX)) {
            return triggerName.substring(0, triggerName.length() - TRIGGER_SUFFIX.length());
        } else {
            return null;
        }
    }

    static private String getTriggerSuffix(final String event) {
        return (event == null ? "" : '_' + event.toLowerCase()) + TRIGGER_SUFFIX;
    }

    public static enum ClauseType {
        ADD_COL, ADD_CONSTRAINT, ADD_INDEX, DROP_COL, DROP_CONSTRAINT, DROP_INDEX, ALTER_COL,
        /**
         * e.g. SET COMMENT.
         */
        OTHER,
        /**
         * e.g. DROP TRIGGER.
         */
        OTHER_DROP,
        /**
         * e.g. CREATE TRIGGER.
         */
        OTHER_ADD;
    }

    public static enum ConcatStep {
        // *_CONSTRAINT must be steps to allow to insert rows in different tables that references
        // each other.
        // *_INDEX should be steps to avoid constantly updating the index when inserting many rows.

        // drop constraints first since, at least in pg, they depend on indexes
        DROP_CONSTRAINT(ClauseType.DROP_CONSTRAINT),
        // drop indexes before columns to avoid having to know if the index is dropped because its
        // columns are dropped
        DROP_INDEX(ClauseType.DROP_INDEX),
        // drop first to allow to drop and re-create, DROP_COL last of DROP clauses since a column
        // doesn't depend on anything (likewise ADD_COL first of ADD clauses), ALTER_COL & OTHER at
        // the end to allow to use new fields
        ALTER_TABLE(ClauseType.OTHER_DROP, ClauseType.DROP_COL, ClauseType.ADD_COL, ClauseType.OTHER_ADD, ClauseType.ALTER_COL, ClauseType.OTHER),
        // likewise add indexes before since constraints need them
        ADD_INDEX(ClauseType.ADD_INDEX), ADD_CONSTRAINT(ClauseType.ADD_CONSTRAINT);

        private final Set<ClauseType> types;

        private ConcatStep(ClauseType... types) {
            this.types = new LinkedHashSet<ClauseType>();
            for (final ClauseType t : types)
                this.types.add(t);
        }

        public final Set<ClauseType> getTypes() {
            return this.types;
        }
    }

    /**
     * Allow to change names of tables.
     * 
     * @author Sylvain
     */
    public static class NameTransformer {

        /**
         * Transformer that does nothing.
         */
        public static final NameTransformer NOP = new NameTransformer();

        /**
         * Called once for each {@link ChangeTable}.
         * 
         * @param tableName the original table name.
         * @return the name that will be used.
         */
        public SQLName transformTableName(final SQLName tableName) {
            return tableName;
        }

        /**
         * Called once for each foreign key.
         * 
         * @param rootName the name of the root of the table.
         * @param tableName the name of the table.
         * @param linkDest the name of the destination table.
         * @return the name that will be used to reference the foreign table.
         */
        public SQLName transformLinkDestTableName(final String rootName, final String tableName, final SQLName linkDest) {
            return transformTableName(linkDest.getItemCount() == 1 ? new SQLName(rootName, linkDest.getName()) : linkDest);
        }
    }

    public static class ChangeRootNameTransformer extends NameTransformer {

        private final String r;

        public ChangeRootNameTransformer(String r) {
            super();
            this.r = r;
        }

        @Override
        public SQLName transformTableName(final SQLName tableName) {
            return new SQLName(this.r, tableName.getName());
        }

        @Override
        public SQLName transformLinkDestTableName(final String rootName, final String tableName, final SQLName linkDest) {
            return linkDest.getItemCount() == 1 ? transformTableName(new SQLName(rootName, linkDest.getName())) : linkDest;
        }
    }

    public static final Set<ClauseType> ORDERED_TYPES;

    static {
        final Set<ClauseType> tmp = new LinkedHashSet<ClauseType>(ClauseType.values().length);
        for (final ConcatStep step : ConcatStep.values())
            tmp.addAll(step.getTypes());
        assert tmp.equals(EnumSet.allOf(ClauseType.class)) : "ConcatStep is missing some types : " + tmp;
        ORDERED_TYPES = Collections.unmodifiableSet(tmp);
    }

    /**
     * Compute the SQL needed to create all passed tables, handling foreign key cycles.
     * 
     * @param cts the tables to create.
     * @param r where to create them.
     * @return the SQL needed.
     */
    public static List<String> cat(List<? extends ChangeTable<?>> cts, final String r) {
        return cat(cts, new ChangeRootNameTransformer(r));
    }

    public static List<String> cat(final List<? extends ChangeTable<?>> cts) {
        return cat(cts, NameTransformer.NOP);
    }

    public static List<String> cat(final List<? extends ChangeTable<?>> cts, final NameTransformer transf) {
        return cat(cts, transf, false);
    }

    /**
     * Compute the SQL needed to create all passed tables split at the passed boundaries. E.g. if
     * you wanted to create tables without constraints, insert some data and then add constraints,
     * you would pass <code>EnumSet.of(ConcatStep.ADD_CONSTRAINT)</code>.
     * 
     * @param cts the tables to create.
     * @param r where to create them.
     * @param boundaries where to split the SQL statements.
     * @return the SQL needed, by definition the list size is one more than <code>boundaries</code>
     *         size, e.g. if no boundaries are passed all SQL will be in one list.
     */
    public static List<List<String>> cat(final Collection<? extends ChangeTable<?>> cts, final String r, final EnumSet<ConcatStep> boundaries) {
        if (r == null)
            throw new NullPointerException("r is null");
        return cat(cts, new ChangeRootNameTransformer(r), boundaries);
    }

    public static List<List<String>> cat(final Collection<? extends ChangeTable<?>> cts, final NameTransformer transf, final EnumSet<ConcatStep> boundaries) {
        final List<List<String>> res = new ArrayList<List<String>>();
        List<String> current = null;
        for (final ConcatStep step : ConcatStep.values()) {
            if (current == null || boundaries.contains(step)) {
                current = new ArrayList<String>();
                res.add(current);
            }
            for (final ChangeTable<?> ct : cts) {
                final String asString = ct.asString(transf, step);
                if (asString != null && asString.length() > 0) {
                    current.add(asString);
                }
            }

        }
        assert res.size() == boundaries.size() + 1;
        return res;
    }

    private static List<String> cat(List<? extends ChangeTable<?>> cts, final NameTransformer transf, final boolean forceCat) {
        final List<String> res = cat(cts, transf, EnumSet.noneOf(ConcatStep.class)).get(0);
        // don't return [""] because the caller might test the size of the result and assume that
        // the DB was changed
        // MySQL needs to have its "alter table add/drop fk" in separate execute()
        // (multiple add would work in 5.0)
        if (!forceCat && (cts.size() == 0 || cts.get(0).getSyntax().getSystem() == SQLSystem.MYSQL))
            return res;
        else
            return Collections.singletonList(CollectionUtils.join(res, "\n"));
    }

    public static String catToString(List<? extends ChangeTable<?>> cts, final String r) {
        return cat(cts, new ChangeRootNameTransformer(r), true).get(0);
    }

    static public String getForeignColumDefaultValue(final SQLTable foreignTable) {
        return foreignTable.getKey().getType().toString(foreignTable.getUndefinedIDNumber());
    }

    // allow to factor column name from table and FCSpec
    public static final class ForeignColSpec {

        static public ForeignColSpec fromCreateTable(SQLCreateTableBase<?> createTable) {
            final List<String> primaryKey = createTable.getPrimaryKey();
            if (primaryKey.size() != 1)
                throw new IllegalArgumentException("Not exactly one field in the foreign primary key : " + primaryKey);
            return new ForeignColSpec(null, new SQLName(createTable.getRootName(), createTable.getName()), primaryKey.get(0), null);
        }

        static public ForeignColSpec fromTable(SQLTable foreignTable) {
            return fromTable(foreignTable, true);
        }

        static public ForeignColSpec fromTable(SQLTable foreignTable, final boolean absolute) {
            if (foreignTable == null)
                throw new NullPointerException("null table");
            final String defaultVal = getForeignColumDefaultValue(foreignTable);
            final SQLName n = absolute ? foreignTable.getSQLName() : new SQLName(foreignTable.getName());
            return new ForeignColSpec(null, n, foreignTable.getKey().getName(), defaultVal);
        }

        private String fk;
        private final SQLName table;
        private final String pk;
        private final String defaultVal;

        public ForeignColSpec(String fk, SQLName table, String pk, String defaultVal) {
            super();
            this.table = table;
            this.setColumnName(fk);
            this.pk = pk;
            this.defaultVal = defaultVal;
        }

        public final ForeignColSpec setColumnNameFromTable() {
            return this.setColumnNameWithSuffix("");
        }

        public final ForeignColSpec setColumnNameWithSuffix(final String suffix) {
            return this.setColumnName(SQLKey.PREFIX + getTable().getName() + (suffix.length() == 0 ? "" : "_" + suffix));
        }

        public final ForeignColSpec setColumnName(final String fk) {
            if (fk == null)
                this.setColumnNameFromTable();
            else
                this.fk = fk;
            return this;
        }

        public final String getColumnName() {
            return this.fk;
        }

        public final SQLName getTable() {
            return this.table;
        }

        public final String getPrimaryKeyName() {
            return this.pk;
        }

        public final String getDefaultVal() {
            return this.defaultVal;
        }

        public final FCSpec createFCSpec(final Rule updateRule, final Rule deleteRule) {
            return new FCSpec(Collections.singletonList(this.getColumnName()), this.getTable(), Collections.singletonList(this.getPrimaryKeyName()), updateRule, deleteRule);
        }
    }

    public static final class FCSpec {

        static public FCSpec createFromLink(final Link l) {
            return createFromLink(l, l.getTarget());
        }

        /**
         * Create an instance using an existing link but pointing to another table.
         * 
         * @param l an existing link, e.g. root1.LOCAL pointing to root1.BATIMENT.
         * @param newDest the new destination for the link, e.g. root2.BATIMENT.
         * @return a new instance, e.g. root1.LOCAL pointing to root2.BATIMENT.
         * @throws IllegalArgumentException if <code>newDest</code> is not compatible with <code>l.
         *         {@link Link#getTarget() getTarget()}</code>.
         */
        static public FCSpec createFromLink(final Link l, final SQLTable newDest) {
            if (newDest != l.getTarget()) {
                final List<SQLField> ffs = l.getFields();
                final Set<SQLField> pks = newDest.getPrimaryKeys();
                if (ffs.size() != pks.size())
                    throw new IllegalArgumentException("Size mismatch : " + ffs + " " + pks);
                int i = 0;
                for (final SQLField pk : pks) {
                    if (!ffs.get(i).getType().equals(pk.getType()))
                        throw new IllegalArgumentException("Type mismatch " + ffs.get(i) + " " + pk);
                    i++;
                }
            }
            return new FCSpec(l.getCols(), newDest.getContextualSQLName(l.getSource()), newDest.getPKsNames(), l.getUpdateRule(), l.getDeleteRule());
        }

        private final List<String> cols;
        private final SQLName refTable;
        private final List<String> refCols;
        private final Rule updateRule, deleteRule;

        public FCSpec(List<String> cols, SQLName refTable, List<String> refCols, final Rule updateRule, final Rule deleteRule) {
            super();
            if (refTable.getItemCount() == 0)
                throw new IllegalArgumentException(refTable + " is empty.");
            this.cols = Collections.unmodifiableList(new ArrayList<String>(cols));
            this.refTable = refTable;
            this.refCols = Collections.unmodifiableList(new ArrayList<String>(refCols));
            this.updateRule = updateRule;
            this.deleteRule = deleteRule;
        }

        public final List<String> getCols() {
            return this.cols;
        }

        public final SQLName getRefTable() {
            return this.refTable;
        }

        public final List<String> getRefCols() {
            return this.refCols;
        }

        public final Rule getUpdateRule() {
            return this.updateRule;
        }

        public final Rule getDeleteRule() {
            return this.deleteRule;
        }
    }

    static protected final class Clauses {
        private final ListMap<ClauseType, DeferredGeneralClause> clauses;

        Clauses() {
            this.clauses = new ListMap<>();
        }

        Clauses(final Clauses clauses) {
            this.clauses = new ListMap<>(clauses.clauses);
        }

        public void reset() {
            this.clauses.clear();
        }

        public final void addClause(final String s, final ClauseType type) {
            this.addClause(-1, s, type);
        }

        public final void addClause(final int index, final String s, final ClauseType type) {
            this.addClause(index, new DeferredGeneralClause() {
                @Override
                public ClauseType getType() {
                    return type;
                }

                @Override
                public String asString(ChangeTable<?> ct, SQLName tableName, NameTransformer transf) {
                    return s;
                }
            });
        }

        public final void addClause(final DeferredGeneralClause s) {
            this.addClause(-1, s);
        }

        public final void addClause(final int index, final DeferredGeneralClause s) {
            if (s != null) {
                if (index < 0) {
                    this.clauses.add(s.getType(), s);
                } else {
                    this.clauses.addAll(s.getType(), Collections.<DeferredGeneralClause> emptyList());
                    this.clauses.get(s.getType()).add(index, s);
                }
            }
        }

        public final void addAllClauses(List<DeferredGeneralClause> clauses) {
            for (final DeferredGeneralClause c : clauses) {
                this.addClause(c);
            }
        }

        private final List<String> getClauses(final ChangeTable<?> ct, final SQLName tableName, final NameTransformer transf, final Set<ClauseType> types, final String intraTypeSep) {
            final ITransformer<DeferredGeneralClause, String> tf = new ITransformer<DeferredGeneralClause, String>() {
                @Override
                public String transformChecked(DeferredGeneralClause input) {
                    return input.asString(ct, tableName, transf);
                }
            };
            final List<String> res = new ArrayList<String>();
            for (final ClauseType type : ORDERED_TYPES) {
                if (types.contains(type)) {
                    final List<DeferredGeneralClause> clauses = this.clauses.getNonNull(type);
                    if (clauses.size() > 0) {
                        if (intraTypeSep == null) {
                            for (final DeferredGeneralClause c : clauses) {
                                res.add(tf.transformChecked(c));
                            }
                        } else {
                            res.add(CollectionUtils.join(clauses, intraTypeSep, tf));
                        }
                    }
                }
            }
            return res;
        }

        // all clauses, not grouped
        protected final List<String> getClauses(final ChangeTable<?> ct, final SQLName tableName, final NameTransformer transf, final Set<ClauseType> types) {
            return this.getClauses(ct, tableName, transf, types, null);
        }

        protected final void appendTo(final StringBuffer res, final ChangeTable<?> ct, final SQLName tableName, final NameTransformer transf, final Set<ClauseType> types) {
            final List<String> outClauses = this.getClauses(ct, tableName, transf, types);
            if (outClauses.size() > 0) {
                res.append("\n\n");
                res.append(CollectionUtils.join(outClauses, "\n"));
            }
        }
    }

    static protected final class InAndOutClauses {
        private final Clauses inClauses;
        private final Clauses outClauses;

        protected InAndOutClauses() {
            this.inClauses = new Clauses();
            this.outClauses = new Clauses();
        }

        protected InAndOutClauses(final InAndOutClauses c) {
            this.inClauses = new Clauses(c.inClauses);
            this.outClauses = new Clauses(c.outClauses);
        }

        public void reset() {
            this.inClauses.reset();
            this.outClauses.reset();
        }

        public boolean isEmpty() {
            return this.inClauses.clauses.isEmpty() && this.outClauses.clauses.isEmpty();
        }

        public Clauses getInClauses() {
            return this.inClauses;
        }

        public Clauses getOutClauses() {
            return this.outClauses;
        }
    }

    private String rootName, name;
    private final SQLSyntax syntax;
    private final List<FCSpec> fks;
    private final InAndOutClauses clauses;

    public ChangeTable(final SQLSyntax syntax, final String rootName, final String name) {
        super();
        this.syntax = syntax;
        this.rootName = rootName;
        this.name = name;
        this.fks = new ArrayList<FCSpec>();
        this.clauses = new InAndOutClauses();

        // check that (T) this; will succeed
        if (this.getClass() != ReflectUtils.getTypeArguments(this, ChangeTable.class).get(0))
            throw new IllegalStateException("illegal subclass: " + this.getClass());
    }

    @SuppressWarnings("unchecked")
    protected final T thisAsT() {
        return (T) this;
    }

    public final SQLSyntax getSyntax() {
        return this.syntax;
    }

    /**
     * Reset this instance's attributes to default values. Ie clauses will be emptied but if the
     * name was changed it won't be changed back to its original value (since it has no default
     * value).
     */
    public void reset() {
        this.fks.clear();
        this.clauses.reset();
    }

    public boolean isEmpty() {
        return this.fks.isEmpty() && this.clauses.isEmpty();
    }

    /**
     * Adds a varchar column not null and with '' as the default.
     * 
     * @param name the name of the column.
     * @param count the number of char.
     * @return this.
     * @throws IllegalArgumentException if <code>count</code> is too high.
     */
    public final T addVarCharColumn(String name, int count) {
        return this.addVarCharColumn(name, count, false);
    }

    /**
     * Adds a varchar column not null and with '' as the default.
     * 
     * @param name the name of the column.
     * @param count the number of characters.
     * @param lenient <code>true</code> if <code>count</code> should be restricted to the
     *        {@link SQLSyntax#getMaximumVarCharLength() maximum} allowed value of the system,
     *        <code>false</code> will throw an exception.
     * @return this.
     * @throws IllegalArgumentException if <code>count</code> is too high and <code>lenient</code>
     *         is <code>false</code>.
     */
    public final T addVarCharColumn(final String name, int count, final boolean lenient) throws IllegalArgumentException {
        return this.addVarCharColumn(name, count, lenient, "''", false);
    }

    /**
     * Adds a varchar column.
     * 
     * @param name the name of the column.
     * @param count the number of characters.
     * @param lenient <code>true</code> if <code>count</code> should be restricted to the
     *        {@link SQLSyntax#getMaximumVarCharLength() maximum} allowed value of the system,
     *        <code>false</code> will throw an exception.
     * @param defaultValue the SQL default value of the column, can be <code>null</code>, e.g. "''".
     * @param nullable whether the column accepts NULL.
     * @return this.
     * @throws IllegalArgumentException if <code>count</code> is too high and <code>lenient</code>
     *         is <code>false</code>.
     */
    public final T addVarCharColumn(final String name, int count, final boolean lenient, final String defaultValue, final boolean nullable) throws IllegalArgumentException {
        final int max = getSyntax().getMaximumVarCharLength();
        if (count > max) {
            if (lenient) {
                Log.get().fine("Truncated " + name + " from " + count + " to " + max);
                count = max;
            } else {
                throw new IllegalArgumentException("Count too high : " + count + " > " + max);
            }
        }
        return this.addColumn(name, "varchar(" + count + ")", defaultValue, nullable);
    }

    public final T addDateAndTimeColumn(String name) {
        return this.addColumn(name, getSyntax().getDateAndTimeType());
    }

    /**
     * Adds a non-null integer column.
     * 
     * @param name the name of the column.
     * @param defaultVal the default value of the column.
     * @return this.
     */
    public final T addIntegerColumn(String name, int defaultVal) {
        return this.addIntegerColumn(name, defaultVal, false);
    }

    /**
     * Adds an integer column.
     * 
     * @param name the name of the column.
     * @param defaultVal the default value of the column, can be <code>null</code>.
     * @param nullable whether the column accepts NULL.
     * @return this.
     */
    public final T addIntegerColumn(String name, Integer defaultVal, boolean nullable) {
        return this.addNumberColumn(name, Integer.class, defaultVal, nullable);
    }

    public final T addLongColumn(String name, Long defaultVal, boolean nullable) {
        return this.addNumberColumn(name, Long.class, defaultVal, nullable);
    }

    public final T addShortColumn(String name, Short defaultVal, boolean nullable) {
        return this.addNumberColumn(name, Short.class, defaultVal, nullable);
    }

    /**
     * Adds a number column.
     * 
     * @param name the name of the column.
     * @param javaType the java class, it must be supported by the {@link #getSyntax() syntax}, e.g.
     *        Double.class.
     * @param defaultVal the default value of the column, can be <code>null</code>, e.g. 3.14.
     * @param nullable whether the column accepts NULL.
     * @return this.
     * @see SQLSyntax#getTypeNames(Class)
     */
    public final <N extends Number> T addNumberColumn(String name, Class<N> javaType, N defaultVal, boolean nullable) {
        final Collection<String> typeNames = getSyntax().getTypeNames(javaType);
        if (typeNames.size() == 0)
            throw new IllegalArgumentException(javaType + " isn't supported by " + getSyntax());
        return this.addColumn(name, typeNames.iterator().next(), getNumberDefault(defaultVal), nullable);
    }

    final String getNumberDefault(final Number defaultVal) {
        return defaultVal == null ? null : defaultVal.toString();
    }

    /**
     * Adds a decimal column.
     * 
     * @param name the name of the column.
     * @param precision the total number of digits.
     * @param scale the number of digits after the decimal point.
     * @param defaultVal the default value of the column, can be <code>null</code>, e.g. 3.14.
     * @param nullable whether the column accepts NULL.
     * @return this.
     * @see SQLSyntax#getDecimal(int, int)
     * @see SQLSyntax#getDecimalIntPart(int, int)
     */
    public final T addDecimalColumn(String name, int precision, int scale, BigDecimal defaultVal, boolean nullable) {
        return this.addColumn(name, getSyntax().getDecimal(precision, scale), getNumberDefault(defaultVal), nullable);
    }

    public final T addBooleanColumn(String name, Boolean defaultVal, boolean nullable) {
        final SQLType boolType = SQLType.getBoolean(getSyntax());
        return this.addColumn(name, boolType.getTypeName(), boolType.toString(defaultVal), nullable);
    }

    /**
     * Adds a column.
     * 
     * @param name the name of the column.
     * @param sqlType the SQL type, e.g. "double precision" or "varchar(32)".
     * @param defaultVal the SQL default value of the column, can be <code>null</code>, e.g. "3.14"
     *        or "'small text'".
     * @param nullable whether the column accepts NULL.
     * @return this.
     */
    public final T addColumn(String name, String sqlType, String defaultVal, boolean nullable) {
        return this.addColumn(name, getSyntax().getFieldDecl(sqlType, defaultVal, nullable));
    }

    public abstract T addColumn(String name, String definition);

    public final T addColumn(SQLField f) {
        return this.addColumn(f.getName(), f);
    }

    public final T addColumn(final String name, SQLField f) {
        return this.addColumn(name, this.getSyntax().getFieldDecl(f));
    }

    public final boolean addIndex(final Index index) {
        // only add index if there won't be an automatic one created. As explained in
        // addForeignConstraint() if we did add one in H2 we would need to drop it explicitly.
        final boolean add = !this.hasAutomaticIndex(index);
        if (add) {
            this.addOutsideClause(getSyntax().getCreateIndex(index));
        }
        return add;
    }

    public final T addForeignConstraint(Link l, boolean createIndex) {
        return this.addForeignConstraint(FCSpec.createFromLink(l), createIndex);
    }

    public final T addForeignConstraint(String fieldName, SQLName refTable, String refCols) {
        return this.addForeignConstraint(singletonList(fieldName), refTable, true, singletonList(refCols));
    }

    /**
     * Adds a foreign constraint specifying that <code>fieldName</code> points to
     * <code>refTable</code>.
     * 
     * @param fieldName a field of this table.
     * @param refTable the destination of <code>fieldName</code>.
     * @param createIndex whether to also create an index on <code>fieldName</code>.
     * @param refCols the columns in <code>refTable</code>.
     * @return this.
     */
    public final T addForeignConstraint(final List<String> fieldName, SQLName refTable, boolean createIndex, List<String> refCols) {
        return this.addForeignConstraint(new FCSpec(fieldName, refTable, refCols, null, null), createIndex);
    }

    // createIndex = false for when we only removeForeignConstraint() without removing the column :
    // - if there's was no index, don't add one
    // - if there's was one automatic index (e.g. H2) it was dropped and will be added again
    // - if there's was one manual index (e.g. pg) it wasn't dropped
    public final T addForeignConstraint(final FCSpec fkSpec, boolean createIndex) {
        this.fks.add(fkSpec);
        // check autoCreatesFKIndex() to avoid creating a duplicate index. Also, on H2, only
        // automatically created indexes are automatically dropped ; so if we added one here we
        // would need to drop it in AlterTable.dropColumn()
        if (createIndex && !getSyntax().getSystem().autoCreatesFKIndex())
            this.addOutsideClause(new DeferredClause() {
                @Override
                public ClauseType getType() {
                    return ClauseType.ADD_INDEX;
                }

                @Override
                public String asString(ChangeTable<?> ct, SQLName tableName) {
                    return ct.getSyntax().getCreateIndex("_fki", tableName, fkSpec.getCols());
                }
            });
        return thisAsT();
    }

    public final T removeForeignConstraint(final FCSpec fkSpec) {
        this.fks.remove(fkSpec);
        return thisAsT();
    }

    public final List<FCSpec> getForeignConstraints() {
        return Collections.unmodifiableList(this.fks);
    }

    // true if a foreign constraint will create an equivalent index
    private final boolean hasAutomaticIndex(final Index i) {
        if (i.isUnique() || !StringUtils.isEmpty(i.getFilter()) || !getSyntax().getSystem().autoCreatesFKIndex())
            return false;
        for (final FCSpec fc : this.fks) {
            if (fc.getCols().equals(i.getCols()))
                return true;
        }
        return false;
    }

    // * addForeignColumn = addColumn + addForeignConstraint

    public T addForeignColumn(SQLCreateTableBase<?> createTable) {
        return this.addForeignColumn(ForeignColSpec.fromCreateTable(createTable));
    }

    /**
     * Add a foreign column to a table not yet created.
     * 
     * @param suffix the suffix of the column, used to tell apart multiple columns pointing to the
     *        same table, e.g. "" or "2".
     * @param createTable the table the new column must point to.
     * @return this.
     * @see #addForeignColumn(String, SQLCreateTableBase)
     */
    public T addForeignColumnWithSuffix(String suffix, SQLCreateTableBase<?> createTable) {
        return this.addForeignColumn(ForeignColSpec.fromCreateTable(createTable).setColumnNameWithSuffix(suffix));
    }

    /**
     * Add a foreign column to a table not yet created. Note: this method assumes that the foreign
     * table will be created in the same root as this table, like with
     * {@link ChangeTable#cat(List, String)}.
     * 
     * @param fk the field name, e.g. "ID_BAT".
     * @param createTable the table the new column must point to.
     * @return this.
     * @see #addForeignColumn(String, SQLName, String, String)
     */
    public T addForeignColumn(String fk, SQLCreateTableBase<?> createTable) {
        return this.addForeignColumn(ForeignColSpec.fromCreateTable(createTable).setColumnName(fk));
    }

    /**
     * Add a column and its foreign constraint. If <code>table</code> is of length 1 it will be
     * prepended the root name of this table.
     * 
     * @param fk the field name, eg "ID_BAT".
     * @param table the name of the referenced table, eg BATIMENT.
     * @param pk the name of the referenced field, eg "ID".
     * @param defaultVal the default value for the column, eg "1".
     * @return this.
     */
    public T addForeignColumn(String fk, SQLName table, String pk, String defaultVal) {
        return this.addForeignColumn(new ForeignColSpec(fk, table, pk, defaultVal));
    }

    public T addForeignColumn(final ForeignColSpec spec) {
        return this.addForeignColumn(spec, null, null);
    }

    public T addForeignColumn(final ForeignColSpec spec, final Rule updateRule, final Rule deleteRule) {
        this.addColumn(spec.getColumnName(), this.getSyntax().getIDType() + " DEFAULT " + spec.getDefaultVal());
        return this.addForeignConstraint(spec.createFCSpec(updateRule, deleteRule), true);
    }

    public T addForeignColumn(String fk, SQLTable foreignTable) {
        return this.addForeignColumn(fk, foreignTable, true);
    }

    /**
     * Add a column and its foreign constraint
     * 
     * @param fk the field name, eg "ID_BAT".
     * @param foreignTable the referenced table, eg /BATIMENT/.
     * @param absolute <code>true</code> if the link should include the whole name of
     *        <code>foreignTable</code>, <code>false</code> if the link should just be its name.
     * @return this.
     * @see #addForeignColumn(String, SQLName, String, String)
     */
    public T addForeignColumn(String fk, SQLTable foreignTable, final boolean absolute) {
        return this.addForeignColumn(ForeignColSpec.fromTable(foreignTable, absolute).setColumnName(fk));
    }

    public T addUniqueConstraint(final String name, final List<String> cols) {
        return this.addUniqueConstraint(name, cols, null);
    }

    /**
     * Add a unique constraint. If the table already exists, an initial check will be performed. As
     * per the standard <code>NULL</code> means unknown and therefore equal with nothing.
     * <p>
     * NOTE: on some systems, an index or even triggers will be created instead (particularly with a
     * where).
     * </p>
     * 
     * @param name name of the constraint.
     * @param cols the columns of the constraint, e.g. ["DESIGNATION"].
     * @param where an optional where to limit the rows checked, can be <code>null</code>, e.g. "not
     *        ARCHIVED".
     * @return this.
     */
    public T addUniqueConstraint(final String name, final List<String> cols, final String where) {
        return this.addUniqueConstraint(name, cols, where, null);
    }

    /**
     * Add a unique constraint with additional data than the other methods. On {@link SQLSystem#H2}
     * {@link UniqueConstraintCreatorHelper#getObject(SQLSyntax)} should return a {@link Trigger}
     * class to be used instead of requiring to compile Java source code. The class name must end
     * with {@value #H2_UNIQUE_TRIGGER_CLASS_SUFFIX} (needed so that {@link SQLTable} can recognize
     * it).
     * 
     * @param name name of the constraint.
     * @param c data about the constraint.
     * @return this.
     * @see #addUniqueConstraint(String, List, String)
     */
    public T addUniqueConstraint(final String name, final UniqueConstraintCreatorHelper c) {
        return this.addUniqueConstraint(name, c.getColumns(), c.getWhere(), c);
    }

    private T addUniqueConstraint(final String name, final List<String> cols, final String where, final UniqueConstraintCreatorHelper c) {
        assert c == null || c.getWhere() == where;
        assert c == null || c.getColumns() == cols;
        final String comment = c == null ? null : c.getComment();

        final int size = cols.size();
        if (size == 0)
            throw new IllegalArgumentException("No cols");
        final SQLSystem system = getSyntax().getSystem();
        // MS treat all NULL equals contrary to the standard
        if (system == SQLSystem.MSSQL) {
            return this.addOutsideClause(createUniquePartialIndex(name, cols, where));
        } else if (where == null) {
            return this.addClause(createUniqueConstraint(name, cols));
        } else if (system == SQLSystem.POSTGRESQL) {
            return this.addOutsideClause(createUniquePartialIndex(name, cols, where));
        } else if (system == SQLSystem.H2) {
            final String body;
            final Class<?> h2TriggerClass = c == null ? null : (Class<?>) c.getObject(getSyntax());
            if (h2TriggerClass == null) {
                final String javaWhere = StringUtils.doubleQuote(where);
                final String javaCols = "java.util.Arrays.asList(" + CollectionUtils.join(cols, ", ", new ITransformer<String, String>() {
                    @Override
                    public String transformChecked(final String col) {
                        return StringUtils.doubleQuote(col);
                    }
                }) + ")";
                body = "AS $$ org.h2.api.Trigger create(){ return new " + PartialUniqueTrigger.class.getName() + "(" + javaCols + ", " + javaWhere + "); } $$";
                assert H2_UNIQUE_TRIGGER_PATTERN.matcher(body).find();
            } else {
                PartialUniqueTrigger h2Trigger;
                try {
                    h2Trigger = (PartialUniqueTrigger) h2TriggerClass.newInstance();
                } catch (Exception e) {
                    throw new IllegalArgumentException("Couldn't instantiate " + h2TriggerClass, e);
                }
                if (!h2Trigger.getColumns().equals(cols) || !Objects.equals(SQLTable.workAroundForH2WhereTrigger(h2Trigger.getWhere()), SQLTable.workAroundForH2WhereTrigger(where)))
                    throw new IllegalArgumentException("Wrong parameters returned by " + h2TriggerClass);
                if (!h2TriggerClass.getSimpleName().endsWith(H2_UNIQUE_TRIGGER_CLASS_SUFFIX))
                    throw new IllegalArgumentException("Class name invalid, must end by \'" + H2_UNIQUE_TRIGGER_CLASS_SUFFIX + "\' : " + h2TriggerClass);
                body = "CALL " + SQLBase.quoteIdentifier(h2TriggerClass.getName());
                // already checked above
                assert H2_UNIQUE_TRIGGER_CLASS_PATTERN.matcher(body).find();
            }
            final UniqueTrigger trigger = new UniqueTrigger(name, Arrays.asList(TRIGGER_EVENTS), comment) {
                @Override
                protected String getBody(SQLName tableName) {
                    return body;
                }

                @Override
                protected String getInitialCheckBody(SQLName tableName) {
                    final String select = getInitialCheckSelect(cols, where, tableName);
                    // TODO should implement SIGNAL instead of abusing CSVREAD
                    return "SELECT CASE WHEN (" + select + ") > 0 then CSVREAD('Unique constraint violation') else 'OK' end case;";
                }
            };
            // initial select to check uniqueness
            if (checkExistingUniqueness()) {
                this.addOutsideClause(trigger.createInitialCheckClause());
            }
            return this.addOutsideClause(trigger);
        } else if (system == SQLSystem.MYSQL) {
            final UniqueTrigger trigger = new UniqueTrigger(name, Arrays.asList(TRIGGER_EVENTS[0]), comment) {
                @Override
                protected String getBody(final SQLName tableName) {
                    final String body = "BEGIN IF " + getNotNullWhere(cols, "NEW.") + " THEN\n" +
                    //
                    "IF ( SELECT COUNT(*) from " + tableName + " where " + where + " and " + CollectionUtils.join(cols, " and ", new ITransformer<String, String>() {
                        @Override
                        public String transformChecked(String col) {
                            return SQLBase.quoteIdentifier(col) + " = NEW." + SQLBase.quoteIdentifier(col);
                        }
                    }) + ") > 1 then\n" + MYSQL_TRIGGER_EXCEPTION + "; END IF; \n"
                    // don't put newline right after semicolon to avoid splitting here
                            + "END IF; \n" + "END";
                    return body;
                }

                @Override
                protected String getInitialCheckBody(SQLName tableName) {
                    final String procName = SQLBase.quoteIdentifier("checkUniqueness_" + tableName.getName());
                    String res = "DROP PROCEDURE IF EXISTS " + procName + ";\n";
                    res += "CREATE PROCEDURE " + procName + "() BEGIN\n";
                    final String select = getInitialCheckSelect(cols, where, tableName);
                    // don't put newline right after semicolon to avoid splitting here
                    res += "IF (" + select + ") > 0 THEN " + MYSQL_TRIGGER_EXCEPTION + "; END IF; \n";
                    res += "END;\n";
                    res += "CALL " + procName + ";";
                    return res;
                }
            };
            // initial select to check uniqueness
            if (checkExistingUniqueness()) {
                this.addOutsideClause(trigger.createInitialCheckClause());
            }

            this.addOutsideClause(trigger);
            for (int i = 1; i < TRIGGER_EVENTS.length; i++) {
                this.addOutsideClause(new UniqueTrigger(name, Arrays.asList(TRIGGER_EVENTS[i]), comment) {
                    @Override
                    protected String getBody(final SQLName tableName) {
                        return trigger.getBody(tableName);
                    }

                    @Override
                    protected String getInitialCheckBody(SQLName tableName) {
                        return trigger.getInitialCheckBody(tableName);
                    }
                });
            }
            return thisAsT();
        } else {
            throw new UnsupportedOperationException("System isn't supported : " + system);
        }
    }

    protected final DeferredClause createUniqueConstraint(final String name, final List<String> cols) {
        return new DeferredClause() {
            @Override
            public String asString(ChangeTable<?> ct, SQLName tableName) {
                return ct.getConstraintPrefix() + "CONSTRAINT " + getQuotedConstraintName(tableName, name) + " UNIQUE (" + SQLSyntax.quoteIdentifiers(cols) + ")";
            }

            @Override
            public ClauseType getType() {
                return ClauseType.ADD_CONSTRAINT;
            }
        };
    }

    // can't be inlined with javac 1.6
    private boolean checkExistingUniqueness() {
        return this instanceof AlterTable;
    }

    protected final DeferredClause createUniquePartialIndex(final String name, final List<String> cols, final String userWhere) {
        // http://stackoverflow.com/questions/767657/how-do-i-create-a-unique-constraint-that-also-allows-nulls
        final Where notNullWhere = getSyntax().getSystem() == SQLSystem.MSSQL ? Where.createRaw(getNotNullWhere(cols)) : null;
        final Where w = Where.and(notNullWhere, Where.createRaw(userWhere));
        return getSyntax().getCreateIndex(new SQLIndex(name, cols, true, true, w.toString()));
    }

    // Null is equal with nothing :
    // http://www.postgresql.org/docs/9.4/static/ddl-constraints.html#DDL-CONSTRAINTS-UNIQUE-CONSTRAINTS
    static private String getNotNullWhere(final List<String> cols) {
        return getNotNullWhere(cols, "");
    }

    static private String getNotNullWhere(final List<String> cols, final String prefix) {
        return CollectionUtils.join(cols, " and ", new ITransformer<String, String>() {
            @Override
            public String transformChecked(String col) {
                return prefix + SQLBase.quoteIdentifier(col) + " IS NOT NULL";
            }
        });
    }

    static protected final String getQuotedConstraintName(final SQLName tableName, final String name) {
        return SQLBase.quoteIdentifier(getIndexName(tableName, name));
    }

    static protected final String getIndexName(final SQLName tableName, final String name) {
        // for many systems (at least pg & h2) constraint names must be unique in a schema
        return SQLSyntax.getSchemaUniqueName(tableName.getName(), name);
    }

    static private SQLName getTriggerName(final SQLName tableName, final String indexName, final String event) {
        // put the trigger in the same schema (tidier and required for MySQL)
        return new SQLName(tableName.getItem(-2), SQLSyntax.getSchemaUniqueName(tableName.getName(), indexName + getTriggerSuffix(event)));
    }

    static private abstract class UniqueTrigger extends DeferredClause {

        private final String indexName;
        private final List<String> events;
        private final String comment;

        public UniqueTrigger(String indexName, final List<String> events, final String comment) {
            super();
            this.indexName = indexName;
            this.events = events;
            this.comment = comment;
        }

        @Override
        public final ClauseType getType() {
            return ClauseType.OTHER_ADD;
        }

        @Override
        public final String asString(ChangeTable<?> ct, SQLName tableName) {
            // if there's only one event, it means the system doesn't support multiple so we need to
            // get a unique trigger name for each one
            final SQLName triggerName = getTriggerName(tableName, this.indexName, CollectionUtils.getSole(this.events));
            final String createTriggerSQL = "CREATE TRIGGER " + triggerName + " AFTER " + CollectionUtils.join(this.events, ", ") + " on " + tableName + " FOR EACH ROW " + this.getBody(tableName)
                    + ';';
            return this.comment == null ? createTriggerSQL : "-- " + this.comment + "\n" + createTriggerSQL;
        }

        protected abstract String getBody(SQLName tableName);

        protected abstract String getInitialCheckBody(SQLName tableName);

        protected final String getInitialCheckSelect(final List<String> cols, final String where, SQLName tableName) {
            final Where notNullWhere = Where.createRaw(getNotNullWhere(cols));
            final Where w = Where.and(notNullWhere, Where.createRaw(where));
            return "SELECT count(*) FROM " + tableName + " where " + w + " group by " + SQLSyntax.quoteIdentifiers(cols) + " having count(*)>1";
        }

        protected final DeferredClause createInitialCheckClause() {
            return new DeferredClause() {
                @Override
                public ClauseType getType() {
                    // same type so that this is executed just before
                    return UniqueTrigger.this.getType();
                }

                @Override
                public String asString(ChangeTable<?> ct, SQLName tableName) {
                    return getInitialCheckBody(tableName);
                }
            };
        }
    }

    static protected final class DropUniqueTrigger extends DeferredClause {

        private final String indexName;
        private final String event;

        protected DropUniqueTrigger(String indexName) {
            this(indexName, null);
        }

        protected DropUniqueTrigger(String indexName, String event) {
            super();
            this.indexName = indexName;
            this.event = event;
        }

        @Override
        public final ClauseType getType() {
            return ClauseType.OTHER_DROP;
        }

        @Override
        public final String asString(ChangeTable<?> ct, SQLName tableName) {
            return "DROP TRIGGER IF EXISTS " + getTriggerName(tableName, this.indexName, this.event) + ";";
        }
    }

    protected abstract String getConstraintPrefix();

    /**
     * Add a clause inside the "CREATE TABLE".
     * 
     * @param s the clause to add, eg "CONSTRAINT c UNIQUE field".
     * @param type type of clause.
     * @return this.
     */
    public final T addClause(String s, final ClauseType type) {
        this.clauses.getInClauses().addClause(s, type);
        return thisAsT();
    }

    protected final InAndOutClauses getClauses() {
        final InAndOutClauses res = new InAndOutClauses(this.clauses);
        this.modifyClauses(res);
        return res;
    }

    protected void modifyClauses(InAndOutClauses res) {
        res.getInClauses().addAllClauses(this.getForeignConstraintsClauses());
    }

    public final T addClause(DeferredClause s) {
        this.clauses.getInClauses().addClause(s);
        return thisAsT();
    }

    /**
     * Add a clause outside the "CREATE TABLE".
     * 
     * @param s the clause to add, <code>null</code> being ignored, e.g. "CREATE INDEX ... ;".
     * @return this.
     */
    public final T addOutsideClause(DeferredClause s) {
        this.clauses.getOutClauses().addClause(s);
        return thisAsT();
    }

    public final String asString() {
        return this.asString(NameTransformer.NOP);
    }

    public final String asString(final String rootName) {
        return this.asString(new ChangeRootNameTransformer(rootName));
    }

    // we can't implement asString() since our subclasses have more parameters
    // so we implement outClausesAsString()
    public abstract String asString(final NameTransformer transf);

    // called by #cat()
    protected abstract String asString(final NameTransformer transf, final ConcatStep step);

    // [ CONSTRAINT "BATIMENT_ID_SITE_fkey" FOREIGN KEY ("ID_SITE") REFERENCES "SITE"("ID") ON
    // DELETE CASCADE; ]
    protected final List<DeferredGeneralClause> getForeignConstraintsClauses() {
        final List<DeferredGeneralClause> res = new ArrayList<>(this.fks.size());
        for (final FCSpec fk : this.fks) {
            res.add(new DeferredGeneralClause() {

                @Override
                public ClauseType getType() {
                    return ClauseType.ADD_CONSTRAINT;
                }

                @Override
                public String asString(ChangeTable<?> ct, SQLName tableName, NameTransformer transf) {
                    // resolve relative path, a table is identified by root.table
                    final SQLName relRefTable = fk.getRefTable();
                    final SQLName refTable = transf.transformLinkDestTableName(getRootName(), getName(), relRefTable);
                    return ct.getConstraintPrefix() + ct.getSyntax().getFK(tableName.getName(), fk.getCols(), refTable, fk.getRefCols(), fk.getUpdateRule(), fk.getDeleteRule());
                }
            });
        }
        return res;
    }

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

    public final String getName() {
        return this.name;
    }

    public final void setName(String name) {
        this.name = name;
    }

    public final String getRootName() {
        return this.rootName;
    }

    // not public since the only use of the NameTransformer parameter is for foreign constraints.
    // Client code can thus use DeferredClause.
    protected static interface DeferredGeneralClause {
        // ct necessary because CREATE TABLE( CONSTRAINT ) can become ALTER TABLE ADD CONSTRAINT
        // necessary since the full name of the table is only known in #asString(String)
        public String asString(final ChangeTable<?> ct, final SQLName tableName, final NameTransformer transf);

        public ClauseType getType();
    }

    public static abstract class DeferredClause implements DeferredGeneralClause {
        protected abstract String asString(final ChangeTable<?> ct, final SQLName tableName);

        public final String asString(final ChangeTable<?> ct, final SQLName tableName, final NameTransformer transf) {
            return this.asString(ct, tableName);
        }
    }
}