OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 156 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright 2011 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.model;

import static org.openconcerto.utils.CollectionUtils.join;

import org.openconcerto.sql.Log;
import org.openconcerto.sql.model.SQLField.Properties;
import org.openconcerto.sql.model.SQLTable.SQLIndex;
import org.openconcerto.sql.model.graph.Link.Rule;
import org.openconcerto.sql.model.graph.TablesMap;
import org.openconcerto.sql.utils.ChangeTable;
import org.openconcerto.sql.utils.ChangeTable.ClauseType;
import org.openconcerto.sql.utils.ChangeTable.DeferredClause;
import org.openconcerto.sql.utils.SQLUtils;
import org.openconcerto.utils.CollectionUtils;
import org.openconcerto.utils.CompareUtils;
import org.openconcerto.utils.ListMap;
import org.openconcerto.utils.NetUtils;
import org.openconcerto.utils.StringUtils;
import org.openconcerto.utils.Tuple2;
import org.openconcerto.utils.cc.ITransformer;
import org.openconcerto.utils.cc.IdentityHashSet;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.math.BigDecimal;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.sql.Types;
import java.text.DateFormatSymbols;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.ibm.icu.text.DateTimePatternGenerator;

import net.jcip.annotations.GuardedBy;

/**
 * A class that abstract the syntax of different SQL systems. Type is an SQL datatype like 'int' or
 * 'varchar', definition is the type plus default and constraints like 'int default 1 not null
 * unique'.
 * 
 * @author Sylvain
 * 
 */
public abstract class SQLSyntax {

    static public final String ORDER_NAME = "ORDRE";
    static public final String ARCHIVE_NAME = "ARCHIVE";
    static public final String ID_NAME = "ID";
    @GuardedBy("this")
    static private final Map<SQLSystem, SQLSyntax> instances = new HashMap<SQLSystem, SQLSyntax>();
    static public final String DATA_EXT = ".txt";

    static protected final String TS_EXTENDED_JAVA_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS000";
    static protected final String TS_BASIC_JAVA_FORMAT = "yyyyMMdd'T'HHmmss.SSS000";

    static private final StringUtils.Escaper DEFAULT_LIKE_ESCAPER = new StringUtils.Escaper('\\', '\\');

    static public final class DateProp {
        static public final String YEAR = new String("year with four digits");
        static public final String MONTH_NAME = new String("full name of the month");
        static public final String MONTH_NUMBER = new String("2 digits number of the month (starting at 1)");
        static public final String DAY_IN_MONTH = new String("2 digits day number in the month");
        static public final String DAY_NAME_IN_WEEK = new String("full name of day");
        static public final String HOUR = new String("hour in day (00-23)");
        static public final String MINUTE = new String("minute in hour");
        static public final String SECOND = new String("second in minute");
        static public final String MICROSECOND = new String("microseconds (000000-999999)");

        static public final Set<String> ALL_INSTANCES = new IdentityHashSet<String>(Arrays.asList(YEAR, MONTH_NAME, MONTH_NUMBER, DAY_IN_MONTH, DAY_NAME_IN_WEEK, HOUR, MINUTE, SECOND, MICROSECOND));
        static public final Set<String> LOCALE_SENSITIVE_INSTANCES = new IdentityHashSet<String>(Arrays.asList(MONTH_NAME, DAY_NAME_IN_WEEK));

        static public final List<String> TIME_SKELETON = Arrays.asList(HOUR, MINUTE, SECOND);
        static public final List<String> SHORT_DATE_SKELETON = Arrays.asList(DAY_IN_MONTH, MONTH_NUMBER, YEAR);
        static public final List<String> LONG_DATE_SKELETON = Arrays.asList(DAY_NAME_IN_WEEK, DAY_IN_MONTH, MONTH_NAME, YEAR);
        static public final List<String> SHORT_DATETIME_SKELETON = Arrays.asList(DAY_IN_MONTH, MONTH_NUMBER, YEAR, HOUR, MINUTE);
        static public final List<String> LONG_DATETIME_SKELETON = Arrays.asList(DAY_NAME_IN_WEEK, DAY_IN_MONTH, MONTH_NAME, YEAR, HOUR, MINUTE, SECOND);

        // pure format (i.e. no literal string)
        static protected final IdentityHashMap<String, String> JAVA_DATE_SPECS_PURE;
        static private final SortedMap<String, String> REVERSE_JAVA_SPEC;
        static private final Pattern REVERSE_SPEC_PATTERN;

        static {
            JAVA_DATE_SPECS_PURE = new IdentityHashMap<String, String>();
            JAVA_DATE_SPECS_PURE.put(YEAR, "yyyy");
            JAVA_DATE_SPECS_PURE.put(MONTH_NAME, "MMMM");
            JAVA_DATE_SPECS_PURE.put(MONTH_NUMBER, "MM");
            JAVA_DATE_SPECS_PURE.put(DAY_IN_MONTH, "dd");
            JAVA_DATE_SPECS_PURE.put(DAY_NAME_IN_WEEK, "EEEE");
            JAVA_DATE_SPECS_PURE.put(HOUR, "HH");
            JAVA_DATE_SPECS_PURE.put(MINUTE, "mm");
            JAVA_DATE_SPECS_PURE.put(SECOND, "ss");

            // reverse, so longer strings come first (e.g. MMMM|MM to match the longer one)
            final SortedMap<String, String> m = new TreeMap<String, String>(Collections.reverseOrder());
            REVERSE_JAVA_SPEC = CollectionUtils.invertMap(m, JAVA_DATE_SPECS_PURE);
            assert REVERSE_JAVA_SPEC.size() == JAVA_DATE_SPECS_PURE.size() : "Duplicate values";
            assert !JAVA_DATE_SPECS_PURE.containsKey(null) : "Null spec";
            assert !JAVA_DATE_SPECS_PURE.containsValue(null) : "Null value";

            REVERSE_SPEC_PATTERN = Pattern.compile(CollectionUtils.join(REVERSE_JAVA_SPEC.keySet(), "|"));
        }

        /**
         * Return the best pattern matching the input skeleton.
         * 
         * @param simpleFormat the fields needed (the string literals are ignored), e.g. [YEAR,
         *        DAY_IN_MONTH, MONTH_NUMBER].
         * @param l the locale needed.
         * @return the best match, e.g. [DAY_IN_MONTH, "/", MONTH_NUMBER, "/", YEAR] for
         *         {@link Locale#FRANCE} , [MONTH_NUMBER, "/", DAY_IN_MONTH, "/", YEAR] for
         *         {@link Locale#US}.
         */
        public static List<String> getBestPattern(final List<String> simpleFormat, final Locale l) {
            final StringBuilder sb = new StringBuilder(128);
            for (final String p : simpleFormat) {
                if (JAVA_DATE_SPECS_PURE.containsKey(p))
                    sb.append(JAVA_DATE_SPECS_PURE.get(p));
                else if (ALL_INSTANCES.contains(p))
                    throw new IllegalArgumentException("Unsupported spec : " + p);
                else
                    // ignore
                    Log.get().log(Level.FINE, "Ignore {0}", p);
            }
            // needs same length so our pattern works
            final String bestPattern = DateTimePatternGenerator.getInstance(l).getBestPattern(sb.toString(), DateTimePatternGenerator.MATCH_ALL_FIELDS_LENGTH);
            return parseJavaPattern(bestPattern);
        }

        static List<String> parseJavaPattern(final String bestPattern) {
            final Matcher matcher = REVERSE_SPEC_PATTERN.matcher(bestPattern);
            final Matcher quotedMatcher = SQLBase.quotedPatrn.matcher(bestPattern);
            final List<String> res = new ArrayList<String>();
            int index = 0;
            while (index < bestPattern.length()) {
                final int quoteIndex = bestPattern.indexOf('\'', index);
                final int endSansQuote = quoteIndex < 0 ? bestPattern.length() : quoteIndex;

                // parse quote-free string
                matcher.region(index, endSansQuote);
                while (matcher.find()) {
                    if (index < matcher.start())
                        res.add(bestPattern.substring(index, matcher.start()));
                    res.add(REVERSE_JAVA_SPEC.get(matcher.group()));
                    index = matcher.end();
                }
                assert index <= endSansQuote : "region() failed";
                if (index < endSansQuote)
                    res.add(bestPattern.substring(index, endSansQuote));
                index = endSansQuote;

                // parse quoted string
                if (index < bestPattern.length()) {
                    quotedMatcher.region(index, bestPattern.length());
                    if (!quotedMatcher.find() || quotedMatcher.start() != quotedMatcher.regionStart())
                        throw new IllegalStateException("Quoted string error : " + bestPattern.substring(quoteIndex));
                    res.add(SQLBase.unquoteStringStd(quotedMatcher.group()));
                    index = quotedMatcher.end();
                }
            }
            return res;
        }
    }

    static public final class CaseBuilder {
        // null for "case when"
        private final String oneExpr;
        private final List<String> expressions;
        private String elseExpression;

        CaseBuilder(final String oneExpr) {
            this.oneExpr = oneExpr;
            this.expressions = new ArrayList<String>();
            this.elseExpression = null;
        }

        public final CaseBuilder addWhen(final String test, final String val) {
            this.expressions.add(test);
            this.expressions.add(val);
            return this;
        }

        public CaseBuilder setElse(String elseExpression) {
            this.elseExpression = elseExpression;
            return this;
        }

        public final String build() {
            if (this.expressions.size() == 0)
                return null;
            final StringBuilder sb = new StringBuilder(150);
            this.build(sb);
            return sb.toString();
        }

        public final void build(final StringBuilder sb) {
            sb.append("CASE ");
            if (this.oneExpr != null) {
                sb.append(this.oneExpr);
                sb.append(' ');
            }
            final int stop = this.expressions.size();
            for (int i = 0; i < stop; i += 2) {
                sb.append("WHEN ");
                sb.append(this.expressions.get(i));
                sb.append(" THEN ");
                sb.append(this.expressions.get(i + 1));
                sb.append(' ');
            }
            if (this.elseExpression != null) {
                sb.append("ELSE ");
                sb.append(this.elseExpression);
                sb.append(' ');
            }
            sb.append("END");
        }
    }

    static public enum ConstraintType {
        CHECK, FOREIGN_KEY("FOREIGN KEY"), PRIMARY_KEY("PRIMARY KEY"), UNIQUE,
        /**
         * Only used by MS SQL.
         */
        DEFAULT;

        private final String sqlName;

        private ConstraintType() {
            this(null);
        }

        private ConstraintType(final String n) {
            this.sqlName = n == null ? name() : n;
        }

        public final String getSqlName() {
            return this.sqlName;
        }

        static public ConstraintType find(final String sqlName) {
            for (final ConstraintType c : values())
                if (c.getSqlName().equals(sqlName))
                    return c;
            throw new IllegalArgumentException("Unknown type: " + sqlName);
        }
    }

    static {
        DEFAULT_LIKE_ESCAPER.add('_', '_');
        DEFAULT_LIKE_ESCAPER.add('%', '%');
    }

    public final static SQLSyntax get(DBStructureItemDB sql) {
        return sql.getDBSystemRoot().getSyntax();
    }

    public final static SQLSyntax get(SQLIdentifier sql) {
        return sql.getDBSystemRoot().getSyntax();
    }

    /**
     * Get the default syntax for the passed system. NOTE : when needing a syntax for system
     * currently accessible, {@link DBSystemRoot#getSyntax()} should be used so that server options
     * can be read. Otherwise constructors of subclasses should be used to specify options.
     * 
     * @param system a SQL system.
     * @return the default syntax.
     */
    synchronized final static SQLSyntax get(SQLSystem system) {
        SQLSyntax res = instances.get(system);
        if (res == null) {
            res = create(system, null);
            if (res == null)
                throw new IllegalArgumentException("unsupported system: " + system);
            instances.put(system, res);
        }
        return res;
    }

    static SQLSyntax create(DBSystemRoot sysRoot) {
        return create(sysRoot.getServer().getSQLSystem(), sysRoot);
    }

    private static SQLSyntax create(final SQLSystem sys, final DBSystemRoot sysRoot) {
        final SQLSyntax res;
        if (sys == SQLSystem.POSTGRESQL)
            res = new SQLSyntaxPG();
        else if (sys == SQLSystem.H2)
            res = new SQLSyntaxH2();
        else if (sys == SQLSystem.MYSQL)
            res = SQLSyntaxMySQL.create(sysRoot);
        else if (sys == SQLSystem.MSSQL)
            res = new SQLSyntaxMS();
        else
            res = null;
        assert res == null || res.getSystem() == sys;
        return res;
    }

    private final SQLSystem sys;
    // list to specify the preferred first
    protected final ListMap<Class<?>, String> typeNames;
    // need identity since we use plain strings
    protected final IdentityHashMap<String, String> dateSpecifiers;

    protected SQLSyntax(final SQLSystem sys, final IdentityHashMap<String, String> dateSpecifiers) {
        this.sys = sys;
        this.typeNames = new ListMap<Class<?>, String>();
        if (!dateSpecifiers.keySet().equals(DateProp.ALL_INSTANCES))
            throw new IllegalArgumentException("Not all instances : " + dateSpecifiers.keySet());
        this.dateSpecifiers = dateSpecifiers;
    }

    /**
     * The aliases for a particular type. The first one is the preferred.
     * 
     * @param clazz the type, e.g. Integer.class.
     * @return the SQL aliases, e.g. {"integer", "int", "int4"}.
     */
    public final Collection<String> getTypeNames(Class<?> clazz) {
        return this.typeNames.getNonNull(clazz);
    }

    public final SQLSystem getSystem() {
        return this.sys;
    }

    /**
     * Quote an SQL string.
     * 
     * @param s an arbitrary string, e.g. "salut\ l'ami".
     * @return the quoted form, e.g. "'salut\\ l''ami'".
     * @see SQLBase#quoteStringStd(String)
     */
    public String quoteString(String s) {
        return SQLBase.quoteStringStd(s);
    }

    public final static String quoteString(SQLSyntax b, String s) {
        return b == null ? SQLBase.quoteStringStd(s) : b.quoteString(s);
    }

    public abstract int getMaximumIdentifierLength();

    public String getInitSystemRoot() {
        // by default: nothing
        return "";
    }

    public String getInitRoot(final String name) {
        // by default: nothing
        return "";
    }

    public abstract boolean isAuto(SQLField f);

    // should return an int4 not null with automatic values
    public abstract String getAuto();

    public String getIDType() {
        return " int";
    }

    /**
     * A non null primary int key with a default value, without 'PRIMARY KEY'. MySQL needs this when
     * changing a primary column (otherwise: "multiple primary keys").
     * 
     * @return the corresponding definition.
     */
    public String getPrimaryIDDefinitionShort() {
        return this.getAuto();
    }

    /**
     * A non null primary int key with a default value.
     * 
     * @return the corresponding definition.
     */
    public final String getPrimaryIDDefinition() {
        return this.getPrimaryIDDefinitionShort() + " PRIMARY KEY";
    }

    public String getArchiveType() {
        return " int";
    }

    public String getArchiveDefinition() {
        return this.getArchiveType() + " DEFAULT 0 NOT NULL";
    }

    public final String getOrderType() {
        return " DECIMAL(" + getOrderPrecision() + "," + getOrderScale() + ")";
    }

    public final int getOrderPrecision() {
        return 16;
    }

    public final int getOrderScale() {
        return 8;
    }

    public final String getOrderDefault() {
        return null;
    }

    public final boolean isOrderNullable() {
        return true;
    }

    public final boolean isOrder(final SQLField f, final boolean checkConstraint) {
        final SQLType type = f.getType();
        if (type.getType() != Types.DECIMAL && type.getType() != Types.NUMERIC)
            return false;
        if (type.getSize() != getOrderPrecision() || ((Number) type.getDecimalDigits()).intValue() != getOrderScale())
            return false;
        // uniqueness might be on more than ORDER alone (e.g. on join tables the owner field is also
        // included)
        if (checkConstraint && f.getTable().getConstraints(ConstraintType.UNIQUE, Collections.singletonList(f.getName()), true).isEmpty())
            return false;

        return f.isNullable() == this.isOrderNullable() && CompareUtils.equals(f.getDefaultValue(), getOrderDefault());
    }

    public final String getOrderDefinition(final boolean includeUnique) {
        return this.getFieldDecl(getOrderType(), getOrderDefault(), isOrderNullable()) + (includeUnique ? " UNIQUE" : "");
    }

    /**
     * How to declare a foreign key constraint.
     * 
     * @param tableName the name of the table where the constraint will be.
     * @param fk the name of the foreign keys, eg ["ID_SITE"].
     * @param refTable the name of the referenced table, eg CTech.SITE.
     * @param referencedFields the fields in the foreign table, eg ["ID"].
     * @param updateRule the update rule, <code>null</code> means use DB default.
     * @param deleteRule the delete rule, <code>null</code> means use DB default.
     * @return a String declaring that <code>fk</code> points to <code>referencedFields</code>.
     */
    public String getFK(final String tableName, final List<String> fk, final SQLName refTable, final List<String> referencedFields, final Rule updateRule, final Rule deleteRule) {
        final String onUpdate = updateRule == null ? "" : " ON UPDATE " + getRuleSQL(updateRule);
        final String onDelete = deleteRule == null ? "" : " ON DELETE " + getRuleSQL(deleteRule);
        // a prefix for the constraint name, since in at least PG and H2, constraints are schema
        // wide not table wide. Moreover we can't use the original link name, as sometimes we copy a
        // table in the same schema.
        if (tableName == null)
            throw new NullPointerException("Null tableName");
        final String name = tableName + '_' + join(fk, "__") + "_fkey";
        final String boundedName = StringUtils.getBoundedLengthString(name, this.getMaximumIdentifierLength());
        return "CONSTRAINT " + SQLBase.quoteIdentifier(boundedName) + " FOREIGN KEY ( " + quoteIdentifiers(fk) + " ) REFERENCES " + refTable.quote() + " ( "
        // don't put ON DELETE CASCADE since it's dangerous, plus MS SQL only supports 1 fk with
        // cascade : http://support.microsoft.com/kb/321843/en-us
                + quoteIdentifiers(referencedFields) + " )" + onUpdate + onDelete;
    }

    protected String getRuleSQL(final Rule r) {
        return r.asString();
    }

    public String getDropFK() {
        return getDropConstraint();
    }

    // to drop a constraint that is not a foreign key, eg unique
    public String getDropConstraint() {
        return "DROP CONSTRAINT ";
    }

    public String getDropPrimaryKey(SQLTable t) {
        return "DROP PRIMARY KEY";
    }

    public abstract String getDropIndex(String name, SQLName tableName);

    public String getCreateIndex(final String indexSuffix, final SQLName tableName, final List<String> fields) {
        // a prefix for the name, since in psql index are schema wide not table wide
        return getCreateIndex(false, tableName.getName() + "_" + join(fields, "__") + indexSuffix, tableName, fields);
    }

    public final String getCreateIndex(final boolean unique, final String indexName, final SQLName tableName, final List<String> fields) {
        // cannot use getCreateIndex(Index i) since Index needs an SQLTable
        final String res = "CREATE" + (unique ? " UNIQUE" : "") + " INDEX " + SQLBase.quoteIdentifier(indexName) + " ON " + tableName.quote();
        return res + " (" + quoteIdentifiers(fields) + ");";
    }

    public final DeferredClause getCreateIndex(final SQLIndex i) {
        return new DeferredClause() {

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

            @Override
            protected String asString(ChangeTable<?> ct, SQLName tableName) {
                // mysql indexes are by table (eg ORDRE INT UNIQUE is called just "ORDRE"),
                // but pg needs names unique in the schema, so make the index start by the
                // tablename
                final String indexName = getSchemaUniqueName(tableName.getName(), i.getName());
                String res = "CREATE" + (i.isUnique() ? " UNIQUE" : "") + " INDEX " + SQLBase.quoteIdentifier(indexName) + " ";
                final String exprs = join(i.getAttrs(), ", ");
                res += ct.getSyntax().getCreateIndex("(" + exprs + ")", tableName, i);
                // filter condition or warning if this doesn't support it
                final boolean neededButUnsupported;
                if (i.getFilter() != null && i.getFilter().length() > 0) {
                    res += " WHERE " + i.getFilter();
                    neededButUnsupported = !ct.getSyntax().getSystem().isIndexFilterConditionSupported();
                } else {
                    neededButUnsupported = false;
                }
                res += ";";
                if (neededButUnsupported) {
                    res = "-- filter condition not supported\n-- " + res;
                    Log.get().warning(res);
                }
                return res;
            }
        };
    }

    /**
     * Just the part after "CREATE UNIQUE INDEX foo ".
     * 
     * @param cols the columns of <code>i</code>, since all systems agree avoid duplication, eg
     *        ("f1", "field2").
     * @param tableName the table where the index should be created, eg "root"."t".
     * @param i the index, do not use its table, use <code>tableName</code>.
     * @return the part after "CREATE UNIQUE INDEX foo ".
     */
    protected String getCreateIndex(final String cols, final SQLName tableName, SQLIndex i) {
        return "ON " + tableName.quote() + cols;
    }

    public boolean isUniqueException(final SQLException exn) {
        return SQLUtils.findWithSQLState(exn).getSQLState().equals("23505");
    }

    public abstract boolean isDeadLockException(final SQLException exn);

    /**
     * Something to be appended to CREATE TABLE statements, like "ENGINE = InnoDB".
     * 
     * @return a String that need to be appended to CREATE TABLE statements.
     */
    public String getCreateTableSuffix() {
        return "";
    }

    public final String getFieldDecl(SQLField f) {
        String res = "";
        final SQLSyntax fs = SQLSyntax.get(f);
        if (fs.isAuto(f))
            res += this.getAuto();
        else {
            final String sqlType = getType(f);
            final String sqlDefault = getDefault(f, sqlType);
            final boolean nullable = getNullable(f);

            res += getFieldDecl(sqlType, sqlDefault, nullable);
        }
        return res;
    }

    public final String getFieldDecl(final String sqlType, final String sqlDefault, final boolean nullable) {
        return sqlType + getDefaultClause(sqlDefault) + getNullableClause(nullable);
    }

    protected final boolean getNullable(SQLField f) {
        // if nullable == null, act like nullable
        return !Boolean.FALSE.equals(f.isNullable());
    }

    public final String getNullableClause(boolean nullable) {
        return nullable ? " " : " NOT NULL ";
    }

    /**
     * The default value for the passed field.
     * 
     * @param f the field.
     * @return the default SQL value, eg "0".
     */
    protected final String getDefault(SQLField f) {
        return this.getDefault(f, getType(f));
    }

    protected final String getDefault(SQLField f, final String sqlType) {
        if (!this.supportsDefault(sqlType))
            return null;
        final String stdDefault = getNormalizedDefault(f);
        return stdDefault == null ? null : this.transfDefault(f, stdDefault);
    }

    static final String getNormalizedDefault(SQLField f) {
        final SQLSyntax fs = SQLSyntax.get(f);
        final String stdDefault = fs.transfDefaultSQL2Common(f);
        if (stdDefault == null) {
            return null;
        } else {
            // for the field date default '2008-12-30'
            // pg will report a default value of '2008-12-30'::date
            // for the field date default '2008-12-30'::date
            // h2 will report a default value of DATE '2008-12-30'
            // to make comparisons possible we thus remove the unnecessary cast
            final String castless;
            final Tuple2<Boolean, String> cast = fs.getCast();
            if (cast == null)
                castless = stdDefault;
            else
                castless = remove(stdDefault, fs.getTypeNames(f.getType().getJavaType()), cast.get0(), cast.get1());
            return castless;
        }
    }

    // find a cast with one of the passed strings and remove it
    // e.g. remove("'a'::varchar", ["char", "varchar"], false, "::") yields 'a'
    private static String remove(final String s, final Collection<String> substrings, final boolean leading, final String sep) {
        final String lowerS = s.toLowerCase();
        String typeCast = null;
        for (final String syn : substrings) {
            typeCast = syn.toLowerCase();
            if (leading)
                typeCast = typeCast + sep;
            else
                typeCast = sep + typeCast;
            if (leading ? lowerS.startsWith(typeCast) : lowerS.endsWith(typeCast)) {
                break;
            } else
                typeCast = null;
        }

        if (typeCast == null)
            return s;
        else if (leading)
            return s.substring(typeCast.length());
        else
            return s.substring(0, s.length() - typeCast.length());
    }

    /**
     * Get the default clause.
     * 
     * @param def the default, e.g. "0" or <code>null</code>.
     * @return the default clause, e.g. " DEFAULT 0" or " ".
     */
    public final String getDefaultClause(final String def) {
        if (def == null)
            return " ";
        else
            return " DEFAULT " + def;
    }

    public final String getType(SQLField f) {
        final SQLSyntax fs = SQLSyntax.get(f);
        final SQLType t = f.getType();

        final String sqlType;
        final String typeName = t.getTypeName().toLowerCase();
        if (typeName.contains("clob")) {
            sqlType = "text";
        } else if (Date.class.isAssignableFrom(t.getJavaType())) {
            // allow getAutoDateType() to return null so that normal systems use normal code path
            // (e.g. to handle TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP)
            if (fs.isAutoDate(f) && this.getAutoDateType(f) != null)
                sqlType = this.getAutoDateType(f);
            else if (typeName.contains("datetime") || typeName.contains("timestamp"))
                sqlType = this.getDateAndTimeType() + (getSystem().isFractionalSecondsSupported() && t.getDecimalDigits() != null ? "(" + t.getDecimalDigits() + ")" : "");
            else
                sqlType = typeName;
        } else if (t.getJavaType() == String.class) {
            if (typeName.contains("text") || typeName.contains("clob")) {
                sqlType = "text";
            } else {
                final String type = typeName.contains("var") ? "varchar" : "char";
                final int size = t.getSize();
                if (size < Integer.MAX_VALUE) {
                    sqlType = type + "(" + size + ")";
                } else {
                    Log.get().warning("Unbounded varchar for " + f.getSQLName());
                    // if (this.getSystem() == SQLSystem.MYSQL)
                    // throw new IllegalStateException("MySQL doesn't support unbounded varchar and
                    // might truncate data if reducing size of " + f.getSQLName());
                    // don't specify size
                    sqlType = type;
                }
            }
        } else if (t.getJavaType() == BigDecimal.class) {
            sqlType = "DECIMAL(" + t.getSize() + "," + t.getDecimalDigits() + ")";
        } else {
            // don't care for qualifiers (like unsigned) they're a pain to maintain across systems
            sqlType = this.getTypeNames(t.getJavaType()).iterator().next();
        }
        return sqlType;
    }

    private boolean isAutoDate(SQLField f) {
        if (f.getDefaultValue() == null)
            return false;

        final String def = getNormalizedDefault(f).toLowerCase();
        return Date.class.isAssignableFrom(f.getType().getJavaType()) && (def.contains("now") || def.contains("current_"));
    }

    /**
     * The date type that support a default value, since some systems don't support defaults for all
     * their types. This implementation simply returns <code>null</code>.
     * 
     * @param f the source field.
     * @return the type that support a default value, <code>null</code> to avoid special treatment.
     */
    protected String getAutoDateType(SQLField f) {
        return null;
    }

    /**
     * The type that store both the date and time. This implementation return the SQL standard
     * "timestamp".
     * 
     * @return the type that store both the date and time.
     */
    public final String getDateAndTimeType() {
        return this.getTypeNames(Timestamp.class).iterator().next();
    }

    /**
     * The maximum number of characters in a column. Can be less than that if there are other
     * columns.
     * 
     * @return the maximum number of characters.
     */
    public abstract int getMaximumVarCharLength();

    protected boolean supportsDefault(String sqlType) {
        return true;
    }

    /**
     * Should transform the passed "common" default to its corresponding value in this syntax. This
     * implementation returns the passed argument.
     * 
     * @param f the field the default is for.
     * @param castless the common default without a cast, e.g. TRUE.
     * @return the default useable in this, e.g. 'true' or 1.
     */
    protected String transfDefault(SQLField f, final String castless) {
        return castless;
    }

    private static final Set<String> nonStandardTimeFunctions = CollectionUtils.createSet("now()", "transaction_timestamp()", "current_timestamp()", "getdate()");
    /** list of columns identifying a field in the resultSet from information_schema.COLUMNS */
    public static final List<String> INFO_SCHEMA_NAMES_KEYS = Arrays.asList("TABLE_SCHEMA", "TABLE_NAME", "COLUMN_NAME");

    // tries to transform SQL to a dialect that all systems can understand
    private String transfDefaultSQL2Common(SQLField f) {
        final String defaultVal = this.transfDefaultJDBC2SQL(f);
        if (defaultVal != null && Date.class.isAssignableFrom(f.getType().getJavaType()) && nonStandardTimeFunctions.contains(defaultVal.trim().toLowerCase()))
            return "CURRENT_TIMESTAMP";
        else if (defaultVal != null && Boolean.class.isAssignableFrom(f.getType().getJavaType()))
            return defaultVal.toUpperCase();
        else
            return defaultVal;
    }

    public String transfDefaultJDBC2SQL(SQLField f) {
        return f.getDefaultValue();
    }

    /**
     * How casts are written. E.g. if this returns [true, " "] casts look like "integer 4".
     * 
     * @return whether type is written before the value and what string is put between type and
     *         value, <code>null</code> if the syntax do no use casts.
     */
    protected abstract Tuple2<Boolean, String> getCast();

    /**
     * How to write a cast.
     * 
     * @param expr the expression to cast.
     * @param type the keyword (some systems don't use regular type names).
     * @return the CAST expression.
     * @see #cast(String, Class)
     */
    public String cast(final String expr, final String type) {
        return "CAST( " + expr + " AS " + type + " )";
    }

    public String cast(final String expr, final Class<?> javaType) {
        return this.cast(expr, this.getTypeNames(javaType).iterator().next());
    }

    public final String cast(final String expr, final SQLType type) {
        return this.cast(expr, type.getJavaType());
    }

    // JDBC says: ordered by NON_UNIQUE, TYPE, INDEX_NAME, and ORDINAL_POSITION
    public List<Map<String, Object>> getIndexInfo(final SQLTable t) throws SQLException {
        final List<?> indexesInfo = t.getDBSystemRoot().getDataSource().useConnection(new ConnectionHandlerNoSetup<List<?>, SQLException>() {
            @Override
            public List<?> handle(SQLDataSource ds) throws SQLException {
                return (List<?>) SQLDataSource.MAP_LIST_HANDLER.handle(ds.getConnection().getMetaData().getIndexInfo(t.getBase().getMDName(), t.getSchema().getName(), t.getName(), false, false));
            }
        });
        final List<Map<String, Object>> res = new ArrayList<Map<String, Object>>(indexesInfo.size());
        for (final Object o : indexesInfo) {
            final Map<?, ?> m = (Map<?, ?>) o;
            // ignore all null rows ; some systems (e.g. MySQL) return a string instead of short
            if (!String.valueOf(DatabaseMetaData.tableIndexStatistic).equals(m.get("TYPE").toString()))
                res.add(this.normalizeIndexInfo(m));
        }
        return res;
    }

    /**
     * Convert the map returned by JDBC getIndexInfo() to a normalized form. Currently the map
     * returned must have :
     * <dl>
     * <dt>NON_UNIQUE</dt>
     * <dd>Boolean</dd>
     * <dt>COLUMN_NAME</dt>
     * <dd>Non quoted string, eg "ch amp"</dd>
     * </dl>
     * 
     * @param m the values returned by
     *        {@link DatabaseMetaData#getIndexInfo(String, String, String, boolean, boolean)}.
     * @return a normalized map.
     */
    protected Map<String, Object> normalizeIndexInfo(final Map<?, ?> m) {
        throw new UnsupportedOperationException();
    }

    // copy the passed map and set all keys to upper case
    // (pg returns lower case)
    protected final Map<String, Object> copyIndexInfoMap(final Map<?, ?> m) {
        final Map<String, Object> res = new HashMap<String, Object>(m.size());
        for (final Entry<?, ?> e : m.entrySet())
            res.put(((String) e.getKey()).toUpperCase(), e.getValue());
        return res;
    }

    public abstract String disableFKChecks(DBRoot b);

    public abstract String enableFKChecks(DBRoot b);

    /**
     * Alter clause to change the default.
     * 
     * @param field the field to change.
     * @param defaut the new default value.
     * @return the SQL clause.
     */
    protected final String setDefault(SQLField field, String defaut) {
        if (defaut == null)
            return "ALTER " + field.getQuotedName() + " DROP DEFAULT";
        else
            return "ALTER COLUMN " + field.getQuotedName() + " SET DEFAULT " + defaut;
    }

    /**
     * Alter clauses to transform <code>f</code> into <code>from</code>.
     * 
     * @param f the field to change.
     * @param from the field to copy.
     * @param toTake which properties of <code>from</code> to copy.
     * @return the SQL clauses.
     */
    public final Map<ClauseType, List<String>> getAlterField(SQLField f, SQLField from, Set<Properties> toTake) {
        if (toTake.size() == 0)
            return Collections.emptyMap();

        final Boolean nullable = toTake.contains(Properties.NULLABLE) ? getNullable(from) : null;
        final String newType;
        if (toTake.contains(Properties.TYPE))
            newType = getType(from);
        // type needed by getDefault()
        else if (toTake.contains(Properties.DEFAULT))
            newType = getType(f);
        else
            newType = null;
        final String newDef = toTake.contains(Properties.DEFAULT) ? getDefault(from, newType) : null;

        return getAlterField(f, toTake, newType, newDef, nullable);
    }

    // cannot rename since some systems won't allow it in the same ALTER TABLE
    public abstract Map<ClauseType, List<String>> getAlterField(SQLField f, Set<Properties> toAlter, String type, String defaultVal, Boolean nullable);

    /**
     * The decimal, arbitrary precision, SQL type.
     * 
     * @param precision the total number of digits.
     * @param scale the number of digits after the decimal point.
     * @return the SQL type.
     * @see #getDecimalIntPart(int, int)
     */
    public String getDecimal(int precision, int scale) {
        return " DECIMAL(" + precision + "," + scale + ")";
    }

    /**
     * The decimal, arbitrary precision, SQL type.
     * 
     * @param intPart the number of digits before the decimal point; NOTE this is not the precision
     *        as in SQL.
     * @param fractionalPart the number of digits after the decimal point.
     * @return the SQL type.
     * @see #getDecimal(int, int)
     */
    public final String getDecimalIntPart(int intPart, int fractionalPart) {
        return getDecimal(intPart + fractionalPart, fractionalPart);
    }

    /**
     * Rename a table. Some systems (e.g. MS SQL) need another query to change schema so this method
     * doesn't support it.
     * 
     * @param table the table to rename.
     * @param newName the new name.
     * @return the SQL statement.
     */
    public String getRenameTable(SQLName table, String newName) {
        return "ALTER TABLE " + table.quote() + " RENAME to " + SQLBase.quoteIdentifier(newName);
    }

    public final String getDropTableIfExists(SQLName name) {
        return getDropTable(name, true, true);
    }

    public String getDropTable(SQLName name, final boolean ifExists, final boolean restrict) {
        return "DROP TABLE " + (ifExists ? "IF EXISTS " : "") + name.quote() + (restrict ? " RESTRICT" : " CASCADE");
    }

    public abstract String getDropRoot(String name);

    public abstract String getCreateRoot(String name);

    /**
     * Load data from files.
     * 
     * @param dir the directory where the files are located.
     * @param r the root where to load.
     * @param tableNames the tables to load or <code>null</code> to load all files in
     *        <code>dir</code>.
     * @param delete <code>true</code> if tables should be emptied before loading.
     * @throws IOException if an error occurs while reading the files.
     * @throws SQLException if an error occurs while loading data into the database.
     */
    public final void loadData(final File dir, final DBRoot r, final Set<String> tableNames, final boolean delete) throws IOException, SQLException {
        this.loadData(dir, r, tableNames, delete, true);
    }

    public final void loadData(final File dir, final DBRoot r, final Set<String> tableNames, final boolean delete, final boolean disableFC) throws IOException, SQLException {
        final List<Tuple2<File, SQLTable>> tables = new ArrayList<Tuple2<File, SQLTable>>();
        if (tableNames == null) {
            for (final File f : dir.listFiles(new FileFilter() {
                @Override
                public boolean accept(File f) {
                    return f.isFile() && f.getName().toLowerCase().endsWith(DATA_EXT);
                }
            })) {
                final String tableName = f.getName().substring(0, f.getName().length() - DATA_EXT.length());
                final SQLTable t = r.getTable(tableName);
                if (t == null)
                    Log.get().warning("table " + tableName + " doesn't exist in " + r);
                else
                    tables.add(Tuple2.create(f, t));
            }
        } else {
            for (final String tableName : tableNames) {
                final File f = new File(dir, tableName + DATA_EXT);
                if (f.exists())
                    tables.add(Tuple2.create(f, r.getTable(tableName)));
                else
                    Log.get().warning(f.getAbsolutePath() + " doesn't exist");
            }
        }
        // only run at the end to avoid being stopped while loading
        if (disableFC)
            r.getBase().getDataSource().execute(disableFKChecks(r));
        for (final Tuple2<File, SQLTable> t : tables)
            loadData(t.get0(), t.get1(), delete, Level.INFO);
        if (disableFC)
            r.getBase().getDataSource().execute(enableFKChecks(r));
    }

    public final void loadData(final File f, final SQLTable t) throws IOException, SQLException {
        this.loadData(f, t, false);
    }

    public final void loadData(final File f, final SQLTable t, final boolean delete) throws IOException, SQLException {
        this.loadData(f, t, delete, null);
    }

    public final void loadData(final File f, final SQLTable t, final boolean delete, final Level level) throws IOException, SQLException {
        if (level != null)
            Log.get().log(level, "loading " + f + " into " + t.getSQLName() + "... ");
        if (delete)
            t.getBase().getDataSource().execute("DELETE FROM " + t.getSQLName().quote());
        _loadData(f, t);
        t.fireTableModified(SQLRow.NONEXISTANT_ID);
        if (level != null)
            Log.get().log(level, "done loading " + f);
    }

    protected abstract void _loadData(File f, SQLTable t) throws IOException, SQLException;

    /**
     * Dump the rows of <code>r</code> to <code>dir</code>. One file per table, named tableName
     * {@link #DATA_EXT} in CSV format (field sep: ",", field delimiter: "\"", line sep: "\n") with
     * the column names on the first line.
     * 
     * @param r the root to dump.
     * @param dir where to dump it.
     * @throws IOException if an error occurred.
     */
    public final void storeData(final DBRoot r, final File dir) throws IOException {
        this.storeData(r, null, dir);
    }

    public final void storeData(final DBRoot r, final Set<String> tableNames, final File dir) throws IOException {
        dir.mkdirs();
        final Map<String, SQLTable> tables = new TreeMap<String, SQLTable>(r.getTablesMap());
        if (tableNames != null)
            tables.keySet().retainAll(tableNames);
        for (final SQLTable t : tables.values()) {
            _storeData(t, new File(dir, t.getName() + DATA_EXT));
        }
    }

    public final void storeData(SQLTable t, File f) throws IOException {
        this._storeData(t, f);
    }

    protected abstract void _storeData(SQLTable t, File f) throws IOException;

    /**
     * Whether the passed server runs on this machine.
     * 
     * @param s the server to test.
     * @return <code>true</code> if this jvm runs on the same machine than <code>s</code>.
     */
    protected boolean isServerLocalhost(SQLServer s) {
        return NetUtils.isSelfAddr(s.getName());
    }

    protected final void checkServerLocalhost(DBStructureItem<?> t) {
        if (!this.isServerLocalhost(t.getServer()))
            throw new IllegalArgumentException("the server of " + t + " is not this computer: " + t.getServer());
    }

    /**
     * The function to return the character with the given ASCII code.
     * 
     * @param asciiCode the code, eg 92 for '\\'.
     * @return the sql function, eg char(92).
     */
    public String getChar(int asciiCode) {
        return "char(" + asciiCode + ")";
    }

    /**
     * The SQL operator to concatenate strings. This returns the standard ||.
     * 
     * @return the cat operator.
     */
    public String getConcatOp() {
        return "||";
    }

    public String getLitteralLikePattern(final String pattern) {
        return DEFAULT_LIKE_ESCAPER.escape(pattern);
    }

    public final String getRegexpOp() {
        return this.getRegexpOp(false);
    }

    /**
     * The SQL operator to match POSIX regular expressions.
     * 
     * @param negation <code>true</code> to negate.
     * @return the regexp operator, <code>null</code> if not supported.
     * @see <a href=
     *      "http://www.postgresql.org/docs/9.1/static/functions-matching.html#FUNCTIONS-POSIX-REGEXP">
     *      postgresql</a>
     */
    public String getRegexpOp(final boolean negation) {
        return negation ? "NOT REGEXP" : "REGEXP";
    }

    /**
     * The SQL needed to create a synonym of <code>t</code> named <code>newName</code>. This can be
     * implemented by updatable views. ATTN for systems using views many restrictions apply (eg no
     * keys, no defaults...).
     * 
     * @param t a table.
     * @param newName the name of the synonym.
     * @return the SQL needed or <code>null</code> if this system doesn't support it.
     */
    public String getCreateSynonym(final SQLTable t, final SQLName newName) {
        return t.getBase().quote("create view %i as select * from %f;", newName, t);
    }

    /**
     * Whether we can put several clauses in one "ALTER TABLE".
     * 
     * @return <code>true</code> if this system supports multiple clauses.
     */
    public boolean supportMultiAlterClause() {
        return true;
    }

    /**
     * Return the SQL clause to compare x and y treating NULL as data.
     * 
     * @param x an sql expression, eg "someField".
     * @param eq <code>true</code> if <code>x</code> and <code>y</code> should be equal, eg
     *        <code>false</code>.
     * @param y an sql expression, eg "1".
     * @return the corresponding clause, eg "someField is distinct from 1".
     */
    public String getNullIsDataComparison(String x, boolean eq, String y) {
        return x + (eq ? " IS NOT DISTINCT FROM " : " IS DISTINCT FROM ") + y;
    }

    public final CaseBuilder createCaseWhenBuilder() {
        return new CaseBuilder(null);
    }

    public final CaseBuilder createCaseBuilder(final String oneExpr) {
        if (oneExpr == null)
            throw new IllegalArgumentException("Missing expression");
        return new CaseBuilder(oneExpr);
    }

    /**
     * Return the SQL expression to get the day of the week for the passed date.
     * 
     * @param sqlTS an SQL expression of type time stamp.
     * @return 1 for Sunday through 7 for Saturday.
     */
    public abstract String getDayOfWeek(final String sqlTS);

    /**
     * Return the SQL expression to get the month of the passed date.
     * 
     * @param sqlTS an SQL expression of type time stamp.
     * @return 1 for January through 12 for December.
     */
    public String getMonth(final String sqlTS) {
        return "MONTH(" + sqlTS + ")";
    }

    public final String getFormatTimestamp(final Timestamp ts, final boolean basic) {
        return this.getFormatTimestamp(SQLType.getFromSyntax(this, Types.TIMESTAMP, 0).toString(ts), basic);
    }

    /**
     * Return the SQL function that format a time stamp to a complete representation. The
     * {@link SimpleDateFormat format} is {@value #TS_EXTENDED_JAVA_FORMAT} : microseconds and no
     * time zone (only format supported by all systems).
     * <p>
     * NOTE : from ISO 8601:2004(E) §4.2.2.4 the decimal sign is included even in basic format.
     * </p>
     * 
     * @param sqlTS an SQL expression of type time stamp.
     * @param basic <code>true</code> if the format should be basic, i.e. with the minimum number of
     *        characters ; <code>false</code> if additional separators must be added (more legible).
     * @return the SQL needed to format the passed parameter.
     */
    public abstract String getFormatTimestamp(final String sqlTS, final boolean basic);

    /**
     * Return the native format from the passed simple one. If names are required, the server locale
     * must be set correctly or {@link #getFormatTimestampSimple(DBRoot, String, List, Locale)}
     * should be used.
     * 
     * @param simpleFormat a list of either {@link DateProp} or literal string to include.
     * @param useServerLocale <code>true</code> to allow the server to format strings (e.g. month
     *        names).
     * @return the native format (to use with {@link #getFormatTimestamp(String, String)}).
     * @throws IllegalArgumentException if <code>useServerLocale</code> is <code>false</code> and
     *         <code>simpleFormat</code> contains some name format properties.
     */
    public final String getTimestampFormat(final List<String> simpleFormat, final boolean useServerLocale) throws IllegalArgumentException {
        final StringBuilder res = new StringBuilder();
        final StringBuilder literal = new StringBuilder();
        for (final String s : simpleFormat) {
            if (!useServerLocale && DateProp.LOCALE_SENSITIVE_INSTANCES.contains(s))
                throw new IllegalArgumentException("passed locale sensitive property : " + s);
            final String spec = this.dateSpecifiers.get(s);
            if (spec == null) {
                // we can't append multiple literal : 'foo' then 'bar' makes 'foo''bar' i.e.
                // "foo'bar" instead of the wanted "foobar"
                literal.append(s);
            } else {
                if (literal.length() > 0) {
                    res.append(this.quoteForTimestampFormat(literal.toString()));
                    literal.setLength(0);
                }
                res.append(spec);
            }
        }
        if (literal.length() > 0) {
            res.append(this.quoteForTimestampFormat(literal.toString()));
        }
        return quoteString(res.toString());
    }

    public abstract String quoteForTimestampFormat(final String text);

    /**
     * Return the SQL expression that format the passed time stamp.
     * 
     * @param sqlTS an SQL expression of type time stamp.
     * @param nativeFormat a SQL varchar for a native format to this syntax, e.g. obtained from
     *        {@link #getTimestampFormat(DBRoot, List, boolean)} .
     * @return the SQL needed to format the passed parameter, e.g. FORMATDATETIME(CURRENT_TIMESTAMP,
     *         'yyyy').
     */
    public abstract String getFormatTimestamp(final String sqlTS, final String nativeFormat);

    public final String getFormatTimestampSimple(String sqlTS, List<String> format) {
        return this.getFormatTimestampSimple(sqlTS, format, Locale.getDefault());
    }

    static private final int[] CALENDAR_DAYS = { Calendar.SUNDAY, Calendar.MONDAY, Calendar.TUESDAY, Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY, Calendar.SATURDAY };

    /**
     * Return the SQL expression that format the passed time stamp.
     * 
     * @param sqlTS an SQL expression of type time stamp.
     * @param simpleFormat a list of either {@link DateProp} or literal string to include, e.g. [
     *        "year is ", {@link DateProp#YEAR}].
     * @param l the locale to use, <code>null</code> meaning use the server.
     * @return the SQL needed to format the passed parameter.
     */
    public final String getFormatTimestampSimple(String sqlTS, List<String> simpleFormat, final Locale l) {
        final boolean useServerLocale = l == null;
        if (!useServerLocale) {
            final List<String> statements = new ArrayList<String>();
            // minimize function calls
            final List<String> nonLocalSensitive = new ArrayList<String>();
            for (final String s : simpleFormat) {
                if (!DateProp.LOCALE_SENSITIVE_INSTANCES.contains(s)) {
                    nonLocalSensitive.add(s);
                } else {
                    if (!nonLocalSensitive.isEmpty()) {
                        statements.add(this.getFormatTimestamp(sqlTS, this.getTimestampFormat(nonLocalSensitive, useServerLocale)));
                        nonLocalSensitive.clear();
                    }
                    final StringBuilder sb = new StringBuilder(512);
                    final DateFormatSymbols symbols = DateFormatSymbols.getInstance(l);
                    if (s == DateProp.DAY_NAME_IN_WEEK) {
                        final CaseBuilder caseBuilder = createCaseBuilder(this.getDayOfWeek(sqlTS));
                        final String[] weekdays = symbols.getWeekdays();
                        for (int j = 0; j < CALENDAR_DAYS.length; j++) {
                            // SQL function begins at 1
                            caseBuilder.addWhen(String.valueOf(j + 1), quoteString(weekdays[CALENDAR_DAYS[j]]));
                        }
                        caseBuilder.setElse(quoteString("unknown week day name"));
                        caseBuilder.build(sb);
                    } else if (s == DateProp.MONTH_NAME) {
                        final CaseBuilder caseBuilder = createCaseBuilder(this.getMonth(sqlTS));
                        int i = 1;
                        for (final String m : symbols.getMonths()) {
                            caseBuilder.addWhen(String.valueOf(i), quoteString(m));
                            i++;
                        }
                        caseBuilder.setElse(quoteString("unknown month name"));
                        caseBuilder.build(sb);
                    } else {
                        throw new IllegalStateException("Unknown prop : " + s);
                    }
                    statements.add(sb.toString());
                }
            }
            if (!nonLocalSensitive.isEmpty()) {
                statements.add(this.getFormatTimestamp(sqlTS, this.getTimestampFormat(nonLocalSensitive, useServerLocale)));
            }
            return CollectionUtils.join(statements, this.getConcatOp());
        } else {
            return this.getFormatTimestamp(sqlTS, this.getTimestampFormat(simpleFormat, useServerLocale));
        }
    }

    public final String getInsertOne(final SQLName tableName, final List<String> fields, String... values) {
        return this.getInsertOne(tableName, fields, Arrays.asList(values));
    }

    public final String getInsertOne(final SQLName tableName, final List<String> fields, final List<String> values) {
        return getInsert(tableName, fields, Collections.singletonList(values));
    }

    public final String getInsert(final SQLName tableName, final List<String> fields, final List<List<String>> values) {
        return "INSERT INTO " + tableName.quote() + "(" + quoteIdentifiers(fields) + ") " + getValues(values, fields.size());
    }

    public final String getValues(final List<List<String>> rows) {
        return this.getValues(rows, -1);
    }

    /**
     * Create a VALUES expression.
     * 
     * @param rows the rows with the SQL expression for each cell.
     * @param colCount the number of columns the rows must have, -1 meaning infer it from
     *        <code>rows</code>.
     * @return the VALUES expression, e.g. "VALUES (1, 'one'), (2, 'two'), (3, 'three')".
     */
    public final String getValues(final List<List<String>> rows, int colCount) {
        final int rowCount = rows.size();
        if (rowCount < 1)
            throw new IllegalArgumentException("Empty rows will cause a syntax error");
        if (colCount < 0)
            colCount = rows.get(0).size();
        final StringBuilder sb = new StringBuilder(rowCount * 64);
        final char space = rowCount > 6 ? '\n' : ' ';
        sb.append("VALUES");
        sb.append(space);
        for (final List<String> row : rows) {
            if (row.size() != colCount)
                throw new IllegalArgumentException("Row have wrong size, not " + colCount + " : " + row);
            sb.append("(");
            sb.append(CollectionUtils.join(row, ", "));
            sb.append("),");
            sb.append(space);
        }
        // remove last ", "
        sb.setLength(sb.length() - 2);
        return sb.toString();
    }

    public final String getConstantTableStatement(final List<List<String>> rows) {
        return this.getConstantTableStatement(rows, -1);
    }

    /**
     * Return a complete statement to return the passed list of rows.
     * 
     * @param rows the rows with the SQL expression for each cell.
     * @param colCount the number of columns the rows must have, -1 meaning infer it from
     *        <code>rows</code>.
     * @return the complete SQL expression that can be executed as is.
     * @see #getValues(List, int)
     * @see #getConstantTable(List, String, List)
     */
    public String getConstantTableStatement(final List<List<String>> rows, int colCount) {
        return this.getValues(rows, colCount);
    }

    /**
     * Get a constant table usable as a join.
     * 
     * @param rows the SQL values for the table, e.g. [["1", "'one'"], ["2", "'two'"]].
     * @param alias the table alias, e.g. "t".
     * @param columnsAlias the columns aliases.
     * @return a constant table, e.g. ( VALUES (1, 'one'), (2, 'two') ) as "t" ("n", "name").
     */
    public String getConstantTable(final List<List<String>> rows, final String alias, final List<String> columnsAlias) {
        final int colSize = columnsAlias.size();
        if (colSize < 1)
            throw new IllegalArgumentException("Empty columns will cause a syntax error");
        final StringBuilder sb = new StringBuilder(rows.size() * 64);
        sb.append("( ");
        sb.append(getValues(rows, colSize));
        sb.append(" ) as ");
        sb.append(SQLBase.quoteIdentifier(alias));
        sb.append(" (");
        for (final String colAlias : columnsAlias) {
            sb.append(SQLBase.quoteIdentifier(colAlias));
            sb.append(", ");
        }
        // remove last ", "
        sb.setLength(sb.length() - 2);
        sb.append(")");
        return sb.toString();
    }

    protected final String getTablesMapJoin(final TablesMap tables, final String schemaExpr, final String tableExpr) {
        final List<List<String>> rows = new ArrayList<List<String>>();
        for (final Entry<String, Set<String>> e : tables.entrySet()) {
            final String schemaName = this.quoteString(e.getKey());
            if (e.getValue() == null) {
                rows.add(Arrays.asList(schemaName, "NULL"));
            } else {
                for (final String tableName : e.getValue())
                    rows.add(Arrays.asList(schemaName, this.quoteString(tableName)));
            }
        }
        final String tableAlias = "tables";
        final SQLName schemaName = new SQLName(tableAlias, "schema");
        final SQLName tableName = new SQLName(tableAlias, "table");

        final String schemaWhere = schemaExpr + " = " + schemaName.quote();
        final String tableWhere = "(" + tableName.quote() + " is null or " + tableExpr + " = " + tableName.quote() + ")";
        return "INNER JOIN " + getConstantTable(rows, tableAlias, Arrays.asList(schemaName.getName(), tableName.getName())) + " on " + schemaWhere + " and " + tableWhere;
    }

    /**
     * A query to retrieve columns metadata from INFORMATION_SCHEMA. The result must have at least
     * {@link #INFO_SCHEMA_NAMES_KEYS}.
     * 
     * @param b the base.
     * @param tables the tables by schemas names.
     * @return the query to retrieve information about columns.
     */
    public abstract String getColumnsQuery(SQLBase b, TablesMap tables);

    /**
     * Return the query to find the functions. The result must have 3 columns : schema, name and src
     * (this should provide the most information possible, eg just the body, the complete SQL or
     * <code>null</code> if nothing can be found).
     * 
     * @param b the base.
     * @param schemas the schemas we're interested in.
     * @return the query or <code>null</code> if no information can be retrieved.
     */
    public abstract String getFunctionQuery(SQLBase b, Set<String> schemas);

    /**
     * Return the constraints in the passed tables.
     * 
     * @param b the base.
     * @param tables the tables by schemas names.
     * @return a list of map with at least "TABLE_SCHEMA", "TABLE_NAME", "CONSTRAINT_NAME",
     *         "CONSTRAINT_TYPE", (List of String)"COLUMN_NAMES" keys and "DEFINITION".
     * @throws SQLException if an error occurs.
     */
    public abstract List<Map<String, Object>> getConstraints(SQLBase b, TablesMap tables) throws SQLException;

    protected final String quoteStrings(Collection<String> c) {
        return CollectionUtils.join(c, ", ", new ITransformer<String, String>() {
            @Override
            public String transformChecked(String s) {
                return quoteString(s);
            }
        });
    }

    public static final String quoteIdentifiers(Collection<String> c) {
        return join(c, ", ", new ITransformer<String, String>() {
            @Override
            public String transformChecked(String s) {
                return SQLBase.quoteIdentifier(s);
            }
        });
    }

    public static final String getSchemaUniqueName(final String tableName, final String name) {
        return name.startsWith(tableName) ? name : tableName + "_" + name;
    }

    /**
     * A query to retrieve triggers in the passed schemas and tables. The result must have at least
     * TRIGGER_NAME, TABLE_SCHEMA, TABLE_NAME, ACTION (system dependent, e.g. "NEW.F = true") and
     * SQL (the SQL needed to create the trigger, can be <code>null</code>).
     * 
     * @param b the base.
     * @param tables the tables by schemas names.
     * @return the query to retrieve triggers.
     * @throws SQLException if an error occurs.
     */
    public abstract String getTriggerQuery(SQLBase b, TablesMap tables) throws SQLException;

    public abstract String getDropTrigger(Trigger t);

    /**
     * The part of an UPDATE query specifying tables and fields to update.
     * 
     * @param t the table whose fields will change.
     * @param tables the other tables of the update.
     * @param setPart the fields of <code>t</code> and their values.
     * @return the SQL specifying how to set the fields.
     * @throws UnsupportedOperationException if this system doesn't support the passed update, e.g.
     *         multi-table.
     */
    public String getUpdate(SQLTable t, List<String> tables, Map<String, String> setPart) throws UnsupportedOperationException {
        String res = t.getSQLName().quote() + " SET\n" + CollectionUtils.join(setPart.entrySet(), ",\n", new ITransformer<Entry<String, String>, String>() {
            @Override
            public String transformChecked(Entry<String, String> input) {
                // pg require that fields are unprefixed
                return SQLBase.quoteIdentifier(input.getKey()) + " = " + input.getValue();
            }
        });
        if (tables.size() > 0)
            res += " FROM " + CollectionUtils.join(tables, ", ");
        return res;
    }

    public DeferredClause getSetTableComment(final String comment) {
        return new DeferredClause() {
            @Override
            public ClauseType getType() {
                return ClauseType.OTHER;
            }

            @Override
            protected String asString(ChangeTable<?> ct, SQLName tableName) {
                return "COMMENT ON TABLE " + tableName.quote() + " IS " + ct.getSyntax().quoteString(comment) + ";";
            }
        };
    }
}