OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 180 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
17 ilm 1
/*
2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3
 *
182 ilm 4
 * Copyright 2011-2019 OpenConcerto, by ILM Informatique. All rights reserved.
17 ilm 5
 *
6
 * The contents of this file are subject to the terms of the GNU General Public License Version 3
7
 * only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
8
 * copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
9
 * language governing permissions and limitations under the License.
10
 *
11
 * When distributing the software, include this License Header Notice in each file.
12
 */
13
 
14
 package org.openconcerto.sql.model;
15
 
16
import static org.openconcerto.utils.CollectionUtils.join;
132 ilm 17
 
17 ilm 18
import org.openconcerto.sql.Log;
19
import org.openconcerto.sql.model.SQLField.Properties;
83 ilm 20
import org.openconcerto.sql.model.SQLTable.SQLIndex;
57 ilm 21
import org.openconcerto.sql.model.graph.Link.Rule;
67 ilm 22
import org.openconcerto.sql.model.graph.TablesMap;
144 ilm 23
import org.openconcerto.sql.utils.ChangeTable;
17 ilm 24
import org.openconcerto.sql.utils.ChangeTable.ClauseType;
144 ilm 25
import org.openconcerto.sql.utils.ChangeTable.DeferredClause;
83 ilm 26
import org.openconcerto.sql.utils.SQLUtils;
17 ilm 27
import org.openconcerto.utils.CollectionUtils;
83 ilm 28
import org.openconcerto.utils.CompareUtils;
29
import org.openconcerto.utils.ListMap;
30
import org.openconcerto.utils.StringUtils;
17 ilm 31
import org.openconcerto.utils.Tuple2;
32
import org.openconcerto.utils.cc.ITransformer;
142 ilm 33
import org.openconcerto.utils.cc.IdentityHashSet;
17 ilm 34
 
35
import java.io.File;
36
import java.io.FileFilter;
37
import java.io.IOException;
38
import java.math.BigDecimal;
182 ilm 39
import java.math.RoundingMode;
17 ilm 40
import java.sql.DatabaseMetaData;
41
import java.sql.SQLException;
67 ilm 42
import java.sql.Timestamp;
17 ilm 43
import java.sql.Types;
142 ilm 44
import java.text.DateFormatSymbols;
67 ilm 45
import java.text.SimpleDateFormat;
17 ilm 46
import java.util.ArrayList;
47
import java.util.Arrays;
142 ilm 48
import java.util.Calendar;
17 ilm 49
import java.util.Collection;
50
import java.util.Collections;
51
import java.util.Date;
52
import java.util.HashMap;
142 ilm 53
import java.util.IdentityHashMap;
17 ilm 54
import java.util.List;
142 ilm 55
import java.util.Locale;
17 ilm 56
import java.util.Map;
57
import java.util.Map.Entry;
58
import java.util.Set;
142 ilm 59
import java.util.SortedMap;
61 ilm 60
import java.util.TreeMap;
67 ilm 61
import java.util.logging.Level;
142 ilm 62
import java.util.regex.Matcher;
63
import java.util.regex.Pattern;
17 ilm 64
 
142 ilm 65
import com.ibm.icu.text.DateTimePatternGenerator;
66
 
67
import net.jcip.annotations.GuardedBy;
68
 
17 ilm 69
/**
70
 * A class that abstract the syntax of different SQL systems. Type is an SQL datatype like 'int' or
71
 * 'varchar', definition is the type plus default and constraints like 'int default 1 not null
72
 * unique'.
73
 *
74
 * @author Sylvain
75
 *
76
 */
77
public abstract class SQLSyntax {
78
 
79
    static public final String ORDER_NAME = "ORDRE";
80
    static public final String ARCHIVE_NAME = "ARCHIVE";
81
    static public final String ID_NAME = "ID";
142 ilm 82
    @GuardedBy("this")
17 ilm 83
    static private final Map<SQLSystem, SQLSyntax> instances = new HashMap<SQLSystem, SQLSyntax>();
84
    static public final String DATA_EXT = ".txt";
85
 
67 ilm 86
    static protected final String TS_EXTENDED_JAVA_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS000";
87
    static protected final String TS_BASIC_JAVA_FORMAT = "yyyyMMdd'T'HHmmss.SSS000";
88
 
83 ilm 89
    static private final StringUtils.Escaper DEFAULT_LIKE_ESCAPER = new StringUtils.Escaper('\\', '\\');
90
 
142 ilm 91
    static public final class DateProp {
92
        static public final String YEAR = new String("year with four digits");
93
        static public final String MONTH_NAME = new String("full name of the month");
94
        static public final String MONTH_NUMBER = new String("2 digits number of the month (starting at 1)");
95
        static public final String DAY_IN_MONTH = new String("2 digits day number in the month");
96
        static public final String DAY_NAME_IN_WEEK = new String("full name of day");
97
        static public final String HOUR = new String("hour in day (00-23)");
98
        static public final String MINUTE = new String("minute in hour");
99
        static public final String SECOND = new String("second in minute");
100
        static public final String MICROSECOND = new String("microseconds (000000-999999)");
101
 
102
        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));
103
        static public final Set<String> LOCALE_SENSITIVE_INSTANCES = new IdentityHashSet<String>(Arrays.asList(MONTH_NAME, DAY_NAME_IN_WEEK));
104
 
105
        static public final List<String> TIME_SKELETON = Arrays.asList(HOUR, MINUTE, SECOND);
106
        static public final List<String> SHORT_DATE_SKELETON = Arrays.asList(DAY_IN_MONTH, MONTH_NUMBER, YEAR);
107
        static public final List<String> LONG_DATE_SKELETON = Arrays.asList(DAY_NAME_IN_WEEK, DAY_IN_MONTH, MONTH_NAME, YEAR);
108
        static public final List<String> SHORT_DATETIME_SKELETON = Arrays.asList(DAY_IN_MONTH, MONTH_NUMBER, YEAR, HOUR, MINUTE);
109
        static public final List<String> LONG_DATETIME_SKELETON = Arrays.asList(DAY_NAME_IN_WEEK, DAY_IN_MONTH, MONTH_NAME, YEAR, HOUR, MINUTE, SECOND);
110
 
111
        // pure format (i.e. no literal string)
112
        static protected final IdentityHashMap<String, String> JAVA_DATE_SPECS_PURE;
113
        static private final SortedMap<String, String> REVERSE_JAVA_SPEC;
114
        static private final Pattern REVERSE_SPEC_PATTERN;
115
 
116
        static {
117
            JAVA_DATE_SPECS_PURE = new IdentityHashMap<String, String>();
118
            JAVA_DATE_SPECS_PURE.put(YEAR, "yyyy");
119
            JAVA_DATE_SPECS_PURE.put(MONTH_NAME, "MMMM");
120
            JAVA_DATE_SPECS_PURE.put(MONTH_NUMBER, "MM");
121
            JAVA_DATE_SPECS_PURE.put(DAY_IN_MONTH, "dd");
122
            JAVA_DATE_SPECS_PURE.put(DAY_NAME_IN_WEEK, "EEEE");
123
            JAVA_DATE_SPECS_PURE.put(HOUR, "HH");
124
            JAVA_DATE_SPECS_PURE.put(MINUTE, "mm");
125
            JAVA_DATE_SPECS_PURE.put(SECOND, "ss");
126
 
127
            // reverse, so longer strings come first (e.g. MMMM|MM to match the longer one)
128
            final SortedMap<String, String> m = new TreeMap<String, String>(Collections.reverseOrder());
129
            REVERSE_JAVA_SPEC = CollectionUtils.invertMap(m, JAVA_DATE_SPECS_PURE);
130
            assert REVERSE_JAVA_SPEC.size() == JAVA_DATE_SPECS_PURE.size() : "Duplicate values";
131
            assert !JAVA_DATE_SPECS_PURE.containsKey(null) : "Null spec";
132
            assert !JAVA_DATE_SPECS_PURE.containsValue(null) : "Null value";
133
 
134
            REVERSE_SPEC_PATTERN = Pattern.compile(CollectionUtils.join(REVERSE_JAVA_SPEC.keySet(), "|"));
135
        }
136
 
137
        /**
138
         * Return the best pattern matching the input skeleton.
139
         *
140
         * @param simpleFormat the fields needed (the string literals are ignored), e.g. [YEAR,
141
         *        DAY_IN_MONTH, MONTH_NUMBER].
142
         * @param l the locale needed.
143
         * @return the best match, e.g. [DAY_IN_MONTH, "/", MONTH_NUMBER, "/", YEAR] for
144
         *         {@link Locale#FRANCE} , [MONTH_NUMBER, "/", DAY_IN_MONTH, "/", YEAR] for
145
         *         {@link Locale#US}.
146
         */
147
        public static List<String> getBestPattern(final List<String> simpleFormat, final Locale l) {
148
            final StringBuilder sb = new StringBuilder(128);
149
            for (final String p : simpleFormat) {
150
                if (JAVA_DATE_SPECS_PURE.containsKey(p))
151
                    sb.append(JAVA_DATE_SPECS_PURE.get(p));
152
                else if (ALL_INSTANCES.contains(p))
153
                    throw new IllegalArgumentException("Unsupported spec : " + p);
154
                else
155
                    // ignore
156
                    Log.get().log(Level.FINE, "Ignore {0}", p);
157
            }
158
            // needs same length so our pattern works
159
            final String bestPattern = DateTimePatternGenerator.getInstance(l).getBestPattern(sb.toString(), DateTimePatternGenerator.MATCH_ALL_FIELDS_LENGTH);
160
            return parseJavaPattern(bestPattern);
161
        }
162
 
163
        static List<String> parseJavaPattern(final String bestPattern) {
164
            final Matcher matcher = REVERSE_SPEC_PATTERN.matcher(bestPattern);
165
            final Matcher quotedMatcher = SQLBase.quotedPatrn.matcher(bestPattern);
166
            final List<String> res = new ArrayList<String>();
167
            int index = 0;
168
            while (index < bestPattern.length()) {
169
                final int quoteIndex = bestPattern.indexOf('\'', index);
170
                final int endSansQuote = quoteIndex < 0 ? bestPattern.length() : quoteIndex;
171
 
172
                // parse quote-free string
173
                matcher.region(index, endSansQuote);
174
                while (matcher.find()) {
175
                    if (index < matcher.start())
176
                        res.add(bestPattern.substring(index, matcher.start()));
177
                    res.add(REVERSE_JAVA_SPEC.get(matcher.group()));
178
                    index = matcher.end();
179
                }
180
                assert index <= endSansQuote : "region() failed";
181
                if (index < endSansQuote)
182
                    res.add(bestPattern.substring(index, endSansQuote));
183
                index = endSansQuote;
184
 
185
                // parse quoted string
186
                if (index < bestPattern.length()) {
187
                    quotedMatcher.region(index, bestPattern.length());
188
                    if (!quotedMatcher.find() || quotedMatcher.start() != quotedMatcher.regionStart())
189
                        throw new IllegalStateException("Quoted string error : " + bestPattern.substring(quoteIndex));
190
                    res.add(SQLBase.unquoteStringStd(quotedMatcher.group()));
191
                    index = quotedMatcher.end();
192
                }
193
            }
194
            return res;
195
        }
196
    }
197
 
198
    static public final class CaseBuilder {
199
        // null for "case when"
200
        private final String oneExpr;
201
        private final List<String> expressions;
202
        private String elseExpression;
203
 
204
        CaseBuilder(final String oneExpr) {
205
            this.oneExpr = oneExpr;
206
            this.expressions = new ArrayList<String>();
207
            this.elseExpression = null;
208
        }
209
 
210
        public final CaseBuilder addWhen(final String test, final String val) {
211
            this.expressions.add(test);
212
            this.expressions.add(val);
213
            return this;
214
        }
215
 
216
        public CaseBuilder setElse(String elseExpression) {
217
            this.elseExpression = elseExpression;
218
            return this;
219
        }
220
 
221
        public final String build() {
222
            if (this.expressions.size() == 0)
223
                return null;
224
            final StringBuilder sb = new StringBuilder(150);
225
            this.build(sb);
226
            return sb.toString();
227
        }
228
 
229
        public final void build(final StringBuilder sb) {
230
            sb.append("CASE ");
231
            if (this.oneExpr != null) {
232
                sb.append(this.oneExpr);
233
                sb.append(' ');
234
            }
235
            final int stop = this.expressions.size();
236
            for (int i = 0; i < stop; i += 2) {
237
                sb.append("WHEN ");
238
                sb.append(this.expressions.get(i));
239
                sb.append(" THEN ");
240
                sb.append(this.expressions.get(i + 1));
241
                sb.append(' ');
242
            }
243
            if (this.elseExpression != null) {
244
                sb.append("ELSE ");
245
                sb.append(this.elseExpression);
246
                sb.append(' ');
247
            }
248
            sb.append("END");
249
        }
250
    }
251
 
17 ilm 252
    static public enum ConstraintType {
83 ilm 253
        CHECK, FOREIGN_KEY("FOREIGN KEY"), PRIMARY_KEY("PRIMARY KEY"), UNIQUE,
254
        /**
255
         * Only used by MS SQL.
256
         */
257
        DEFAULT;
17 ilm 258
 
259
        private final String sqlName;
260
 
261
        private ConstraintType() {
262
            this(null);
263
        }
264
 
265
        private ConstraintType(final String n) {
266
            this.sqlName = n == null ? name() : n;
267
        }
268
 
269
        public final String getSqlName() {
270
            return this.sqlName;
271
        }
272
 
273
        static public ConstraintType find(final String sqlName) {
274
            for (final ConstraintType c : values())
275
                if (c.getSqlName().equals(sqlName))
276
                    return c;
41 ilm 277
            throw new IllegalArgumentException("Unknown type: " + sqlName);
17 ilm 278
        }
279
    }
280
 
281
    static {
83 ilm 282
        DEFAULT_LIKE_ESCAPER.add('_', '_');
283
        DEFAULT_LIKE_ESCAPER.add('%', '%');
17 ilm 284
    }
285
 
142 ilm 286
    public final static SQLSyntax get(DBStructureItemDB sql) {
287
        return sql.getDBSystemRoot().getSyntax();
17 ilm 288
    }
289
 
142 ilm 290
    public final static SQLSyntax get(SQLIdentifier sql) {
291
        return sql.getDBSystemRoot().getSyntax();
17 ilm 292
    }
293
 
142 ilm 294
    /**
295
     * Get the default syntax for the passed system. NOTE : when needing a syntax for system
296
     * currently accessible, {@link DBSystemRoot#getSyntax()} should be used so that server options
297
     * can be read. Otherwise constructors of subclasses should be used to specify options.
298
     *
299
     * @param system a SQL system.
300
     * @return the default syntax.
301
     */
302
    synchronized final static SQLSyntax get(SQLSystem system) {
303
        SQLSyntax res = instances.get(system);
17 ilm 304
        if (res == null) {
142 ilm 305
            res = create(system, null);
306
            if (res == null)
307
                throw new IllegalArgumentException("unsupported system: " + system);
308
            instances.put(system, res);
17 ilm 309
        }
310
        return res;
311
    }
312
 
142 ilm 313
    static SQLSyntax create(DBSystemRoot sysRoot) {
314
        return create(sysRoot.getServer().getSQLSystem(), sysRoot);
315
    }
316
 
317
    private static SQLSyntax create(final SQLSystem sys, final DBSystemRoot sysRoot) {
318
        final SQLSyntax res;
319
        if (sys == SQLSystem.POSTGRESQL)
320
            res = new SQLSyntaxPG();
321
        else if (sys == SQLSystem.H2)
182 ilm 322
            res = SQLSyntaxH2.create(sysRoot);
142 ilm 323
        else if (sys == SQLSystem.MYSQL)
324
            res = SQLSyntaxMySQL.create(sysRoot);
325
        else if (sys == SQLSystem.MSSQL)
326
            res = new SQLSyntaxMS();
327
        else
328
            res = null;
329
        assert res == null || res.getSystem() == sys;
330
        return res;
331
    }
332
 
17 ilm 333
    private final SQLSystem sys;
83 ilm 334
    // list to specify the preferred first
335
    protected final ListMap<Class<?>, String> typeNames;
142 ilm 336
    // need identity since we use plain strings
337
    protected final IdentityHashMap<String, String> dateSpecifiers;
17 ilm 338
 
142 ilm 339
    protected SQLSyntax(final SQLSystem sys, final IdentityHashMap<String, String> dateSpecifiers) {
17 ilm 340
        this.sys = sys;
83 ilm 341
        this.typeNames = new ListMap<Class<?>, String>();
142 ilm 342
        if (!dateSpecifiers.keySet().equals(DateProp.ALL_INSTANCES))
343
            throw new IllegalArgumentException("Not all instances : " + dateSpecifiers.keySet());
344
        this.dateSpecifiers = dateSpecifiers;
17 ilm 345
    }
346
 
347
    /**
83 ilm 348
     * The aliases for a particular type. The first one is the preferred.
17 ilm 349
     *
350
     * @param clazz the type, e.g. Integer.class.
351
     * @return the SQL aliases, e.g. {"integer", "int", "int4"}.
352
     */
83 ilm 353
    public final Collection<String> getTypeNames(Class<?> clazz) {
354
        return this.typeNames.getNonNull(clazz);
17 ilm 355
    }
356
 
357
    public final SQLSystem getSystem() {
358
        return this.sys;
359
    }
360
 
142 ilm 361
    /**
362
     * Quote an SQL string.
363
     *
364
     * @param s an arbitrary string, e.g. "salut\ l'ami".
365
     * @return the quoted form, e.g. "'salut\\ l''ami'".
366
     * @see SQLBase#quoteStringStd(String)
367
     */
368
    public String quoteString(String s) {
369
        return SQLBase.quoteStringStd(s);
370
    }
371
 
372
    public final static String quoteString(SQLSyntax b, String s) {
373
        return b == null ? SQLBase.quoteStringStd(s) : b.quoteString(s);
374
    }
375
 
132 ilm 376
    public abstract int getMaximumIdentifierLength();
377
 
83 ilm 378
    public String getInitSystemRoot() {
379
        // by default: nothing
380
        return "";
381
    }
382
 
17 ilm 383
    public String getInitRoot(final String name) {
384
        // by default: nothing
385
        return "";
386
    }
387
 
388
    public abstract boolean isAuto(SQLField f);
389
 
80 ilm 390
    // should return an int4 not null with automatic values
17 ilm 391
    public abstract String getAuto();
392
 
393
    public String getIDType() {
394
        return " int";
395
    }
396
 
397
    /**
398
     * A non null primary int key with a default value, without 'PRIMARY KEY'. MySQL needs this when
399
     * changing a primary column (otherwise: "multiple primary keys").
400
     *
401
     * @return the corresponding definition.
402
     */
403
    public String getPrimaryIDDefinitionShort() {
404
        return this.getAuto();
405
    }
406
 
407
    /**
408
     * A non null primary int key with a default value.
409
     *
410
     * @return the corresponding definition.
411
     */
412
    public final String getPrimaryIDDefinition() {
413
        return this.getPrimaryIDDefinitionShort() + " PRIMARY KEY";
414
    }
415
 
416
    public String getArchiveType() {
417
        return " int";
418
    }
419
 
420
    public String getArchiveDefinition() {
421
        return this.getArchiveType() + " DEFAULT 0 NOT NULL";
422
    }
423
 
424
    public final String getOrderType() {
425
        return " DECIMAL(" + getOrderPrecision() + "," + getOrderScale() + ")";
426
    }
427
 
428
    public final int getOrderPrecision() {
429
        return 16;
430
    }
431
 
432
    public final int getOrderScale() {
433
        return 8;
434
    }
435
 
144 ilm 436
    public final String getOrderDefault() {
17 ilm 437
        return null;
438
    }
439
 
144 ilm 440
    public final boolean isOrderNullable() {
441
        return true;
442
    }
443
 
444
    public final boolean isOrder(final SQLField f, final boolean checkConstraint) {
17 ilm 445
        final SQLType type = f.getType();
446
        if (type.getType() != Types.DECIMAL && type.getType() != Types.NUMERIC)
447
            return false;
448
        if (type.getSize() != getOrderPrecision() || ((Number) type.getDecimalDigits()).intValue() != getOrderScale())
449
            return false;
144 ilm 450
        // uniqueness might be on more than ORDER alone (e.g. on join tables the owner field is also
451
        // included)
452
        if (checkConstraint && f.getTable().getConstraints(ConstraintType.UNIQUE, Collections.singletonList(f.getName()), true).isEmpty())
453
            return false;
17 ilm 454
 
144 ilm 455
        return f.isNullable() == this.isOrderNullable() && CompareUtils.equals(f.getDefaultValue(), getOrderDefault());
17 ilm 456
    }
457
 
144 ilm 458
    public final String getOrderDefinition(final boolean includeUnique) {
459
        return this.getFieldDecl(getOrderType(), getOrderDefault(), isOrderNullable()) + (includeUnique ? " UNIQUE" : "");
17 ilm 460
    }
461
 
462
    /**
463
     * How to declare a foreign key constraint.
464
     *
132 ilm 465
     * @param tableName the name of the table where the constraint will be.
17 ilm 466
     * @param fk the name of the foreign keys, eg ["ID_SITE"].
467
     * @param refTable the name of the referenced table, eg CTech.SITE.
468
     * @param referencedFields the fields in the foreign table, eg ["ID"].
57 ilm 469
     * @param updateRule the update rule, <code>null</code> means use DB default.
470
     * @param deleteRule the delete rule, <code>null</code> means use DB default.
17 ilm 471
     * @return a String declaring that <code>fk</code> points to <code>referencedFields</code>.
472
     */
132 ilm 473
    public String getFK(final String tableName, final List<String> fk, final SQLName refTable, final List<String> referencedFields, final Rule updateRule, final Rule deleteRule) {
57 ilm 474
        final String onUpdate = updateRule == null ? "" : " ON UPDATE " + getRuleSQL(updateRule);
475
        final String onDelete = deleteRule == null ? "" : " ON DELETE " + getRuleSQL(deleteRule);
132 ilm 476
        // a prefix for the constraint name, since in at least PG and H2, constraints are schema
477
        // wide not table wide. Moreover we can't use the original link name, as sometimes we copy a
478
        // table in the same schema.
479
        if (tableName == null)
480
            throw new NullPointerException("Null tableName");
481
        final String name = tableName + '_' + join(fk, "__") + "_fkey";
482
        final String boundedName = StringUtils.getBoundedLengthString(name, this.getMaximumIdentifierLength());
483
        return "CONSTRAINT " + SQLBase.quoteIdentifier(boundedName) + " FOREIGN KEY ( " + quoteIdentifiers(fk) + " ) REFERENCES " + refTable.quote() + " ( "
17 ilm 484
        // don't put ON DELETE CASCADE since it's dangerous, plus MS SQL only supports 1 fk with
485
        // cascade : http://support.microsoft.com/kb/321843/en-us
57 ilm 486
                + quoteIdentifiers(referencedFields) + " )" + onUpdate + onDelete;
17 ilm 487
    }
488
 
57 ilm 489
    protected String getRuleSQL(final Rule r) {
490
        return r.asString();
491
    }
492
 
17 ilm 493
    public String getDropFK() {
494
        return getDropConstraint();
495
    }
496
 
497
    // to drop a constraint that is not a foreign key, eg unique
498
    public String getDropConstraint() {
499
        return "DROP CONSTRAINT ";
500
    }
501
 
41 ilm 502
    public String getDropPrimaryKey(SQLTable t) {
503
        return "DROP PRIMARY KEY";
504
    }
505
 
17 ilm 506
    public abstract String getDropIndex(String name, SQLName tableName);
507
 
508
    public String getCreateIndex(final String indexSuffix, final SQLName tableName, final List<String> fields) {
509
        // a prefix for the name, since in psql index are schema wide not table wide
510
        return getCreateIndex(false, tableName.getName() + "_" + join(fields, "__") + indexSuffix, tableName, fields);
511
    }
512
 
513
    public final String getCreateIndex(final boolean unique, final String indexName, final SQLName tableName, final List<String> fields) {
514
        // cannot use getCreateIndex(Index i) since Index needs an SQLTable
515
        final String res = "CREATE" + (unique ? " UNIQUE" : "") + " INDEX " + SQLBase.quoteIdentifier(indexName) + " ON " + tableName.quote();
516
        return res + " (" + quoteIdentifiers(fields) + ");";
517
    }
518
 
144 ilm 519
    public final DeferredClause getCreateIndex(final SQLIndex i) {
520
        return new DeferredClause() {
17 ilm 521
 
522
            @Override
523
            public ClauseType getType() {
524
                return ClauseType.ADD_INDEX;
525
            }
526
 
527
            @Override
144 ilm 528
            protected String asString(ChangeTable<?> ct, SQLName tableName) {
17 ilm 529
                // mysql indexes are by table (eg ORDRE INT UNIQUE is called just "ORDRE"),
530
                // but pg needs names unique in the schema, so make the index start by the
531
                // tablename
532
                final String indexName = getSchemaUniqueName(tableName.getName(), i.getName());
533
                String res = "CREATE" + (i.isUnique() ? " UNIQUE" : "") + " INDEX " + SQLBase.quoteIdentifier(indexName) + " ";
83 ilm 534
                final String exprs = join(i.getAttrs(), ", ");
144 ilm 535
                res += ct.getSyntax().getCreateIndex("(" + exprs + ")", tableName, i);
17 ilm 536
                // filter condition or warning if this doesn't support it
83 ilm 537
                final boolean neededButUnsupported;
17 ilm 538
                if (i.getFilter() != null && i.getFilter().length() > 0) {
539
                    res += " WHERE " + i.getFilter();
144 ilm 540
                    neededButUnsupported = !ct.getSyntax().getSystem().isIndexFilterConditionSupported();
17 ilm 541
                } else {
83 ilm 542
                    neededButUnsupported = false;
17 ilm 543
                }
544
                res += ";";
83 ilm 545
                if (neededButUnsupported) {
17 ilm 546
                    res = "-- filter condition not supported\n-- " + res;
547
                    Log.get().warning(res);
548
                }
549
                return res;
550
            }
551
        };
552
    }
553
 
554
    /**
555
     * Just the part after "CREATE UNIQUE INDEX foo ".
556
     *
557
     * @param cols the columns of <code>i</code>, since all systems agree avoid duplication, eg
558
     *        ("f1", "field2").
559
     * @param tableName the table where the index should be created, eg "root"."t".
560
     * @param i the index, do not use its table, use <code>tableName</code>.
561
     * @return the part after "CREATE UNIQUE INDEX foo ".
562
     */
83 ilm 563
    protected String getCreateIndex(final String cols, final SQLName tableName, SQLIndex i) {
80 ilm 564
        return "ON " + tableName.quote() + cols;
17 ilm 565
    }
566
 
83 ilm 567
    public boolean isUniqueException(final SQLException exn) {
568
        return SQLUtils.findWithSQLState(exn).getSQLState().equals("23505");
569
    }
570
 
132 ilm 571
    public abstract boolean isDeadLockException(final SQLException exn);
572
 
17 ilm 573
    /**
182 ilm 574
     * Whether the cause of the passed exception is a table not being found.
575
     *
576
     * @param exn an exception.
577
     * @return <code>true</code> if the cause is a table not being found.
578
     */
579
    public abstract boolean isTableNotFoundException(final Exception exn);
580
 
581
    /**
582
     * How to set the amount of time any statement waits while attempting to acquire a lock.
583
     * <p>
584
     * NOTE : some systems only support seconds precision, in that case the amount will be
585
     * {@link RoundingMode#UP rounded up} so that the statement waits at least the passed amount.
586
     * </p>
587
     * <p>
588
     * <strong>Warning</strong> : some systems will wait 2 or 3 times the passed amount.
589
     * </p>
590
     *
591
     * @param millis the number of milliseconds.
592
     * @return the SQL query.
593
     */
594
    public String getSetLockTimeoutQuery(final int millis) {
595
        return "SET lock_timeout " + millis;
596
    }
597
 
598
    public String getShowLockTimeoutQuery() {
599
        return "SELECT lock_timeout()";
600
    }
601
 
602
    /**
17 ilm 603
     * Something to be appended to CREATE TABLE statements, like "ENGINE = InnoDB".
604
     *
605
     * @return a String that need to be appended to CREATE TABLE statements.
606
     */
607
    public String getCreateTableSuffix() {
608
        return "";
609
    }
610
 
611
    public final String getFieldDecl(SQLField f) {
612
        String res = "";
142 ilm 613
        final SQLSyntax fs = SQLSyntax.get(f);
17 ilm 614
        if (fs.isAuto(f))
615
            res += this.getAuto();
616
        else {
617
            final String sqlType = getType(f);
618
            final String sqlDefault = getDefault(f, sqlType);
619
            final boolean nullable = getNullable(f);
620
 
67 ilm 621
            res += getFieldDecl(sqlType, sqlDefault, nullable);
17 ilm 622
        }
623
        return res;
624
    }
625
 
67 ilm 626
    public final String getFieldDecl(final String sqlType, final String sqlDefault, final boolean nullable) {
627
        return sqlType + getDefaultClause(sqlDefault) + getNullableClause(nullable);
628
    }
629
 
17 ilm 630
    protected final boolean getNullable(SQLField f) {
631
        // if nullable == null, act like nullable
632
        return !Boolean.FALSE.equals(f.isNullable());
633
    }
634
 
20 ilm 635
    public final String getNullableClause(boolean nullable) {
17 ilm 636
        return nullable ? " " : " NOT NULL ";
637
    }
638
 
639
    /**
640
     * The default value for the passed field.
641
     *
642
     * @param f the field.
643
     * @return the default SQL value, eg "0".
644
     */
645
    protected final String getDefault(SQLField f) {
646
        return this.getDefault(f, getType(f));
647
    }
648
 
649
    protected final String getDefault(SQLField f, final String sqlType) {
83 ilm 650
        if (!this.supportsDefault(sqlType))
651
            return null;
652
        final String stdDefault = getNormalizedDefault(f);
653
        return stdDefault == null ? null : this.transfDefault(f, stdDefault);
654
    }
655
 
656
    static final String getNormalizedDefault(SQLField f) {
142 ilm 657
        final SQLSyntax fs = SQLSyntax.get(f);
17 ilm 658
        final String stdDefault = fs.transfDefaultSQL2Common(f);
83 ilm 659
        if (stdDefault == null) {
17 ilm 660
            return null;
83 ilm 661
        } else {
17 ilm 662
            // for the field date default '2008-12-30'
663
            // pg will report a default value of '2008-12-30'::date
664
            // for the field date default '2008-12-30'::date
665
            // h2 will report a default value of DATE '2008-12-30'
666
            // to make comparisons possible we thus remove the unnecessary cast
667
            final String castless;
668
            final Tuple2<Boolean, String> cast = fs.getCast();
669
            if (cast == null)
670
                castless = stdDefault;
671
            else
672
                castless = remove(stdDefault, fs.getTypeNames(f.getType().getJavaType()), cast.get0(), cast.get1());
83 ilm 673
            return castless;
17 ilm 674
        }
675
    }
676
 
677
    // find a cast with one of the passed strings and remove it
678
    // e.g. remove("'a'::varchar", ["char", "varchar"], false, "::") yields 'a'
679
    private static String remove(final String s, final Collection<String> substrings, final boolean leading, final String sep) {
680
        final String lowerS = s.toLowerCase();
681
        String typeCast = null;
682
        for (final String syn : substrings) {
683
            typeCast = syn.toLowerCase();
684
            if (leading)
685
                typeCast = typeCast + sep;
686
            else
687
                typeCast = sep + typeCast;
688
            if (leading ? lowerS.startsWith(typeCast) : lowerS.endsWith(typeCast)) {
689
                break;
690
            } else
691
                typeCast = null;
692
        }
693
 
694
        if (typeCast == null)
695
            return s;
696
        else if (leading)
697
            return s.substring(typeCast.length());
698
        else
699
            return s.substring(0, s.length() - typeCast.length());
700
    }
701
 
702
    /**
703
     * Get the default clause.
704
     *
180 ilm 705
     * @param def the default, e.g. "0" or <code>null</code>.
706
     * @return the default clause, e.g. " DEFAULT 0" or " ".
17 ilm 707
     */
20 ilm 708
    public final String getDefaultClause(final String def) {
17 ilm 709
        if (def == null)
710
            return " ";
711
        else
712
            return " DEFAULT " + def;
713
    }
714
 
41 ilm 715
    public final String getType(SQLField f) {
142 ilm 716
        final SQLSyntax fs = SQLSyntax.get(f);
17 ilm 717
        final SQLType t = f.getType();
718
 
719
        final String sqlType;
720
        final String typeName = t.getTypeName().toLowerCase();
721
        if (typeName.contains("clob")) {
722
            sqlType = "text";
723
        } else if (Date.class.isAssignableFrom(t.getJavaType())) {
724
            // allow getAutoDateType() to return null so that normal systems use normal code path
725
            // (e.g. to handle TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP)
726
            if (fs.isAutoDate(f) && this.getAutoDateType(f) != null)
727
                sqlType = this.getAutoDateType(f);
728
            else if (typeName.contains("datetime") || typeName.contains("timestamp"))
729
                sqlType = this.getDateAndTimeType() + (getSystem().isFractionalSecondsSupported() && t.getDecimalDigits() != null ? "(" + t.getDecimalDigits() + ")" : "");
730
            else
731
                sqlType = typeName;
732
        } else if (t.getJavaType() == String.class) {
733
            if (typeName.contains("text") || typeName.contains("clob")) {
734
                sqlType = "text";
735
            } else {
736
                final String type = typeName.contains("var") ? "varchar" : "char";
21 ilm 737
                final int size = t.getSize();
738
                if (size < Integer.MAX_VALUE) {
739
                    sqlType = type + "(" + size + ")";
740
                } else {
741
                    Log.get().warning("Unbounded varchar for " + f.getSQLName());
182 ilm 742
                    if (this.getSystem() == SQLSystem.MYSQL)
743
                        throw new IllegalStateException("MySQL doesn't support unbounded varchar and might truncate data if reducing size of " + f.getSQLName());
21 ilm 744
                    // don't specify size
745
                    sqlType = type;
746
                }
17 ilm 747
            }
748
        } else if (t.getJavaType() == BigDecimal.class) {
749
            sqlType = "DECIMAL(" + t.getSize() + "," + t.getDecimalDigits() + ")";
750
        } else {
142 ilm 751
            // don't care for qualifiers (like unsigned) they're a pain to maintain across systems
752
            sqlType = this.getTypeNames(t.getJavaType()).iterator().next();
17 ilm 753
        }
754
        return sqlType;
755
    }
756
 
757
    private boolean isAutoDate(SQLField f) {
758
        if (f.getDefaultValue() == null)
759
            return false;
760
 
83 ilm 761
        final String def = getNormalizedDefault(f).toLowerCase();
17 ilm 762
        return Date.class.isAssignableFrom(f.getType().getJavaType()) && (def.contains("now") || def.contains("current_"));
763
    }
764
 
765
    /**
766
     * The date type that support a default value, since some systems don't support defaults for all
767
     * their types. This implementation simply returns <code>null</code>.
768
     *
769
     * @param f the source field.
770
     * @return the type that support a default value, <code>null</code> to avoid special treatment.
771
     */
772
    protected String getAutoDateType(SQLField f) {
773
        return null;
774
    }
775
 
776
    /**
777
     * The type that store both the date and time. This implementation return the SQL standard
778
     * "timestamp".
779
     *
780
     * @return the type that store both the date and time.
781
     */
142 ilm 782
    public final String getDateAndTimeType() {
783
        return this.getTypeNames(Timestamp.class).iterator().next();
17 ilm 784
    }
785
 
83 ilm 786
    /**
787
     * The maximum number of characters in a column. Can be less than that if there are other
788
     * columns.
789
     *
790
     * @return the maximum number of characters.
791
     */
792
    public abstract int getMaximumVarCharLength();
793
 
17 ilm 794
    protected boolean supportsDefault(String sqlType) {
795
        return true;
796
    }
797
 
798
    /**
799
     * Should transform the passed "common" default to its corresponding value in this syntax. This
800
     * implementation returns the passed argument.
801
     *
802
     * @param f the field the default is for.
803
     * @param castless the common default without a cast, e.g. TRUE.
804
     * @return the default useable in this, e.g. 'true' or 1.
805
     */
806
    protected String transfDefault(SQLField f, final String castless) {
807
        return castless;
808
    }
809
 
83 ilm 810
    private static final Set<String> nonStandardTimeFunctions = CollectionUtils.createSet("now()", "transaction_timestamp()", "current_timestamp()", "getdate()");
17 ilm 811
    /** list of columns identifying a field in the resultSet from information_schema.COLUMNS */
812
    public static final List<String> INFO_SCHEMA_NAMES_KEYS = Arrays.asList("TABLE_SCHEMA", "TABLE_NAME", "COLUMN_NAME");
813
 
814
    // tries to transform SQL to a dialect that all systems can understand
815
    private String transfDefaultSQL2Common(SQLField f) {
816
        final String defaultVal = this.transfDefaultJDBC2SQL(f);
817
        if (defaultVal != null && Date.class.isAssignableFrom(f.getType().getJavaType()) && nonStandardTimeFunctions.contains(defaultVal.trim().toLowerCase()))
818
            return "CURRENT_TIMESTAMP";
819
        else if (defaultVal != null && Boolean.class.isAssignableFrom(f.getType().getJavaType()))
820
            return defaultVal.toUpperCase();
821
        else
822
            return defaultVal;
823
    }
824
 
825
    public String transfDefaultJDBC2SQL(SQLField f) {
83 ilm 826
        return f.getDefaultValue();
17 ilm 827
    }
828
 
829
    /**
830
     * How casts are written. E.g. if this returns [true, " "] casts look like "integer 4".
831
     *
832
     * @return whether type is written before the value and what string is put between type and
833
     *         value, <code>null</code> if the syntax do no use casts.
834
     */
835
    protected abstract Tuple2<Boolean, String> getCast();
836
 
142 ilm 837
    /**
838
     * How to write a cast.
839
     *
840
     * @param expr the expression to cast.
841
     * @param type the keyword (some systems don't use regular type names).
842
     * @return the CAST expression.
843
     * @see #cast(String, Class)
844
     */
93 ilm 845
    public String cast(final String expr, final String type) {
846
        return "CAST( " + expr + " AS " + type + " )";
847
    }
848
 
142 ilm 849
    public String cast(final String expr, final Class<?> javaType) {
850
        return this.cast(expr, this.getTypeNames(javaType).iterator().next());
851
    }
852
 
853
    public final String cast(final String expr, final SQLType type) {
854
        return this.cast(expr, type.getJavaType());
855
    }
856
 
17 ilm 857
    // JDBC says: ordered by NON_UNIQUE, TYPE, INDEX_NAME, and ORDINAL_POSITION
858
    public List<Map<String, Object>> getIndexInfo(final SQLTable t) throws SQLException {
859
        final List<?> indexesInfo = t.getDBSystemRoot().getDataSource().useConnection(new ConnectionHandlerNoSetup<List<?>, SQLException>() {
860
            @Override
861
            public List<?> handle(SQLDataSource ds) throws SQLException {
862
                return (List<?>) SQLDataSource.MAP_LIST_HANDLER.handle(ds.getConnection().getMetaData().getIndexInfo(t.getBase().getMDName(), t.getSchema().getName(), t.getName(), false, false));
863
            }
864
        });
865
        final List<Map<String, Object>> res = new ArrayList<Map<String, Object>>(indexesInfo.size());
866
        for (final Object o : indexesInfo) {
867
            final Map<?, ?> m = (Map<?, ?>) o;
868
            // ignore all null rows ; some systems (e.g. MySQL) return a string instead of short
869
            if (!String.valueOf(DatabaseMetaData.tableIndexStatistic).equals(m.get("TYPE").toString()))
870
                res.add(this.normalizeIndexInfo(m));
871
        }
872
        return res;
873
    }
874
 
875
    /**
876
     * Convert the map returned by JDBC getIndexInfo() to a normalized form. Currently the map
877
     * returned must have :
878
     * <dl>
879
     * <dt>NON_UNIQUE</dt>
880
     * <dd>Boolean</dd>
881
     * <dt>COLUMN_NAME</dt>
882
     * <dd>Non quoted string, eg "ch amp"</dd>
883
     * </dl>
884
     *
885
     * @param m the values returned by
886
     *        {@link DatabaseMetaData#getIndexInfo(String, String, String, boolean, boolean)}.
887
     * @return a normalized map.
888
     */
889
    protected Map<String, Object> normalizeIndexInfo(final Map<?, ?> m) {
890
        throw new UnsupportedOperationException();
891
    }
892
 
893
    // copy the passed map and set all keys to upper case
894
    // (pg returns lower case)
895
    protected final Map<String, Object> copyIndexInfoMap(final Map<?, ?> m) {
896
        final Map<String, Object> res = new HashMap<String, Object>(m.size());
897
        for (final Entry<?, ?> e : m.entrySet())
898
            res.put(((String) e.getKey()).toUpperCase(), e.getValue());
899
        return res;
900
    }
901
 
902
    public abstract String disableFKChecks(DBRoot b);
903
 
904
    public abstract String enableFKChecks(DBRoot b);
905
 
906
    /**
907
     * Alter clause to change the default.
908
     *
909
     * @param field the field to change.
910
     * @param defaut the new default value.
911
     * @return the SQL clause.
912
     */
913
    protected final String setDefault(SQLField field, String defaut) {
914
        if (defaut == null)
73 ilm 915
            return "ALTER " + field.getQuotedName() + " DROP DEFAULT";
17 ilm 916
        else
73 ilm 917
            return "ALTER COLUMN " + field.getQuotedName() + " SET DEFAULT " + defaut;
17 ilm 918
    }
919
 
920
    /**
921
     * Alter clauses to transform <code>f</code> into <code>from</code>.
922
     *
923
     * @param f the field to change.
924
     * @param from the field to copy.
925
     * @param toTake which properties of <code>from</code> to copy.
926
     * @return the SQL clauses.
927
     */
83 ilm 928
    public final Map<ClauseType, List<String>> getAlterField(SQLField f, SQLField from, Set<Properties> toTake) {
17 ilm 929
        if (toTake.size() == 0)
83 ilm 930
            return Collections.emptyMap();
17 ilm 931
 
932
        final Boolean nullable = toTake.contains(Properties.NULLABLE) ? getNullable(from) : null;
933
        final String newType;
934
        if (toTake.contains(Properties.TYPE))
935
            newType = getType(from);
936
        // type needed by getDefault()
937
        else if (toTake.contains(Properties.DEFAULT))
938
            newType = getType(f);
939
        else
940
            newType = null;
941
        final String newDef = toTake.contains(Properties.DEFAULT) ? getDefault(from, newType) : null;
942
 
943
        return getAlterField(f, toTake, newType, newDef, nullable);
944
    }
945
 
946
    // cannot rename since some systems won't allow it in the same ALTER TABLE
83 ilm 947
    public abstract Map<ClauseType, List<String>> getAlterField(SQLField f, Set<Properties> toAlter, String type, String defaultVal, Boolean nullable);
17 ilm 948
 
80 ilm 949
    /**
950
     * The decimal, arbitrary precision, SQL type.
951
     *
952
     * @param precision the total number of digits.
953
     * @param scale the number of digits after the decimal point.
954
     * @return the SQL type.
955
     * @see #getDecimalIntPart(int, int)
956
     */
17 ilm 957
    public String getDecimal(int precision, int scale) {
958
        return " DECIMAL(" + precision + "," + scale + ")";
959
    }
960
 
80 ilm 961
    /**
962
     * The decimal, arbitrary precision, SQL type.
963
     *
964
     * @param intPart the number of digits before the decimal point; NOTE this is not the precision
965
     *        as in SQL.
966
     * @param fractionalPart the number of digits after the decimal point.
967
     * @return the SQL type.
968
     * @see #getDecimal(int, int)
969
     */
17 ilm 970
    public final String getDecimalIntPart(int intPart, int fractionalPart) {
971
        return getDecimal(intPart + fractionalPart, fractionalPart);
972
    }
973
 
83 ilm 974
    /**
975
     * Rename a table. Some systems (e.g. MS SQL) need another query to change schema so this method
976
     * doesn't support it.
977
     *
978
     * @param table the table to rename.
979
     * @param newName the new name.
980
     * @return the SQL statement.
981
     */
982
    public String getRenameTable(SQLName table, String newName) {
983
        return "ALTER TABLE " + table.quote() + " RENAME to " + SQLBase.quoteIdentifier(newName);
984
    }
985
 
93 ilm 986
    public final String getDropTableIfExists(SQLName name) {
987
        return getDropTable(name, true, true);
83 ilm 988
    }
989
 
93 ilm 990
    public String getDropTable(SQLName name, final boolean ifExists, final boolean restrict) {
991
        return "DROP TABLE " + (ifExists ? "IF EXISTS " : "") + name.quote() + (restrict ? " RESTRICT" : " CASCADE");
992
    }
993
 
17 ilm 994
    public abstract String getDropRoot(String name);
995
 
996
    public abstract String getCreateRoot(String name);
997
 
998
    /**
999
     * Load data from files.
1000
     *
1001
     * @param dir the directory where the files are located.
1002
     * @param r the root where to load.
1003
     * @param tableNames the tables to load or <code>null</code> to load all files in
1004
     *        <code>dir</code>.
1005
     * @param delete <code>true</code> if tables should be emptied before loading.
1006
     * @throws IOException if an error occurs while reading the files.
1007
     * @throws SQLException if an error occurs while loading data into the database.
1008
     */
1009
    public final void loadData(final File dir, final DBRoot r, final Set<String> tableNames, final boolean delete) throws IOException, SQLException {
93 ilm 1010
        this.loadData(dir, r, tableNames, delete, true);
1011
    }
1012
 
1013
    public final void loadData(final File dir, final DBRoot r, final Set<String> tableNames, final boolean delete, final boolean disableFC) throws IOException, SQLException {
17 ilm 1014
        final List<Tuple2<File, SQLTable>> tables = new ArrayList<Tuple2<File, SQLTable>>();
1015
        if (tableNames == null) {
1016
            for (final File f : dir.listFiles(new FileFilter() {
1017
                @Override
1018
                public boolean accept(File f) {
1019
                    return f.isFile() && f.getName().toLowerCase().endsWith(DATA_EXT);
1020
                }
1021
            })) {
1022
                final String tableName = f.getName().substring(0, f.getName().length() - DATA_EXT.length());
1023
                final SQLTable t = r.getTable(tableName);
1024
                if (t == null)
1025
                    Log.get().warning("table " + tableName + " doesn't exist in " + r);
1026
                else
1027
                    tables.add(Tuple2.create(f, t));
1028
            }
1029
        } else {
1030
            for (final String tableName : tableNames) {
1031
                final File f = new File(dir, tableName + DATA_EXT);
1032
                if (f.exists())
1033
                    tables.add(Tuple2.create(f, r.getTable(tableName)));
1034
                else
1035
                    Log.get().warning(f.getAbsolutePath() + " doesn't exist");
1036
            }
1037
        }
1038
        // only run at the end to avoid being stopped while loading
93 ilm 1039
        if (disableFC)
1040
            r.getBase().getDataSource().execute(disableFKChecks(r));
17 ilm 1041
        for (final Tuple2<File, SQLTable> t : tables)
67 ilm 1042
            loadData(t.get0(), t.get1(), delete, Level.INFO);
93 ilm 1043
        if (disableFC)
1044
            r.getBase().getDataSource().execute(enableFKChecks(r));
17 ilm 1045
    }
1046
 
1047
    public final void loadData(final File f, final SQLTable t) throws IOException, SQLException {
1048
        this.loadData(f, t, false);
1049
    }
1050
 
1051
    public final void loadData(final File f, final SQLTable t, final boolean delete) throws IOException, SQLException {
67 ilm 1052
        this.loadData(f, t, delete, null);
1053
    }
1054
 
1055
    public final void loadData(final File f, final SQLTable t, final boolean delete, final Level level) throws IOException, SQLException {
1056
        if (level != null)
1057
            Log.get().log(level, "loading " + f + " into " + t.getSQLName() + "... ");
17 ilm 1058
        if (delete)
1059
            t.getBase().getDataSource().execute("DELETE FROM " + t.getSQLName().quote());
1060
        _loadData(f, t);
182 ilm 1061
        t.fireTableModified();
67 ilm 1062
        if (level != null)
1063
            Log.get().log(level, "done loading " + f);
17 ilm 1064
    }
1065
 
1066
    protected abstract void _loadData(File f, SQLTable t) throws IOException, SQLException;
1067
 
1068
    /**
1069
     * Dump the rows of <code>r</code> to <code>dir</code>. One file per table, named tableName
1070
     * {@link #DATA_EXT} in CSV format (field sep: ",", field delimiter: "\"", line sep: "\n") with
1071
     * the column names on the first line.
1072
     *
1073
     * @param r the root to dump.
1074
     * @param dir where to dump it.
83 ilm 1075
     * @throws IOException if an error occurred.
17 ilm 1076
     */
83 ilm 1077
    public final void storeData(final DBRoot r, final File dir) throws IOException {
17 ilm 1078
        this.storeData(r, null, dir);
1079
    }
1080
 
83 ilm 1081
    public final void storeData(final DBRoot r, final Set<String> tableNames, final File dir) throws IOException {
17 ilm 1082
        dir.mkdirs();
61 ilm 1083
        final Map<String, SQLTable> tables = new TreeMap<String, SQLTable>(r.getTablesMap());
1084
        if (tableNames != null)
1085
            tables.keySet().retainAll(tableNames);
1086
        for (final SQLTable t : tables.values()) {
17 ilm 1087
            _storeData(t, new File(dir, t.getName() + DATA_EXT));
1088
        }
1089
    }
1090
 
83 ilm 1091
    public final void storeData(SQLTable t, File f) throws IOException {
17 ilm 1092
        this._storeData(t, f);
1093
    }
1094
 
83 ilm 1095
    protected abstract void _storeData(SQLTable t, File f) throws IOException;
17 ilm 1096
 
1097
    protected final void checkServerLocalhost(DBStructureItem<?> t) {
182 ilm 1098
        if (!t.getServer().isLocalhost())
17 ilm 1099
            throw new IllegalArgumentException("the server of " + t + " is not this computer: " + t.getServer());
1100
    }
1101
 
1102
    /**
1103
     * The function to return the character with the given ASCII code.
1104
     *
1105
     * @param asciiCode the code, eg 92 for '\\'.
1106
     * @return the sql function, eg char(92).
1107
     */
1108
    public String getChar(int asciiCode) {
1109
        return "char(" + asciiCode + ")";
1110
    }
1111
 
1112
    /**
1113
     * The SQL operator to concatenate strings. This returns the standard ||.
1114
     *
1115
     * @return the cat operator.
1116
     */
1117
    public String getConcatOp() {
1118
        return "||";
1119
    }
1120
 
83 ilm 1121
    public String getLitteralLikePattern(final String pattern) {
1122
        return DEFAULT_LIKE_ESCAPER.escape(pattern);
1123
    }
1124
 
26 ilm 1125
    public final String getRegexpOp() {
1126
        return this.getRegexpOp(false);
1127
    }
1128
 
17 ilm 1129
    /**
26 ilm 1130
     * The SQL operator to match POSIX regular expressions.
1131
     *
1132
     * @param negation <code>true</code> to negate.
1133
     * @return the regexp operator, <code>null</code> if not supported.
132 ilm 1134
     * @see <a href=
1135
     *      "http://www.postgresql.org/docs/9.1/static/functions-matching.html#FUNCTIONS-POSIX-REGEXP">
1136
     *      postgresql</a>
26 ilm 1137
     */
1138
    public String getRegexpOp(final boolean negation) {
1139
        return negation ? "NOT REGEXP" : "REGEXP";
1140
    }
1141
 
1142
    /**
17 ilm 1143
     * The SQL needed to create a synonym of <code>t</code> named <code>newName</code>. This can be
1144
     * implemented by updatable views. ATTN for systems using views many restrictions apply (eg no
1145
     * keys, no defaults...).
1146
     *
1147
     * @param t a table.
1148
     * @param newName the name of the synonym.
1149
     * @return the SQL needed or <code>null</code> if this system doesn't support it.
1150
     */
1151
    public String getCreateSynonym(final SQLTable t, final SQLName newName) {
1152
        return t.getBase().quote("create view %i as select * from %f;", newName, t);
1153
    }
1154
 
1155
    /**
1156
     * Whether we can put several clauses in one "ALTER TABLE".
1157
     *
1158
     * @return <code>true</code> if this system supports multiple clauses.
1159
     */
1160
    public boolean supportMultiAlterClause() {
1161
        return true;
1162
    }
1163
 
1164
    /**
1165
     * Return the SQL clause to compare x and y treating NULL as data.
1166
     *
1167
     * @param x an sql expression, eg "someField".
1168
     * @param eq <code>true</code> if <code>x</code> and <code>y</code> should be equal, eg
1169
     *        <code>false</code>.
1170
     * @param y an sql expression, eg "1".
1171
     * @return the corresponding clause, eg "someField is distinct from 1".
1172
     */
67 ilm 1173
    public String getNullIsDataComparison(String x, boolean eq, String y) {
1174
        return x + (eq ? " IS NOT DISTINCT FROM " : " IS DISTINCT FROM ") + y;
1175
    }
17 ilm 1176
 
142 ilm 1177
    public final CaseBuilder createCaseWhenBuilder() {
1178
        return new CaseBuilder(null);
1179
    }
1180
 
1181
    public final CaseBuilder createCaseBuilder(final String oneExpr) {
1182
        if (oneExpr == null)
1183
            throw new IllegalArgumentException("Missing expression");
1184
        return new CaseBuilder(oneExpr);
1185
    }
1186
 
1187
    /**
1188
     * Return the SQL expression to get the day of the week for the passed date.
1189
     *
1190
     * @param sqlTS an SQL expression of type time stamp.
1191
     * @return 1 for Sunday through 7 for Saturday.
1192
     */
1193
    public abstract String getDayOfWeek(final String sqlTS);
1194
 
1195
    /**
1196
     * Return the SQL expression to get the month of the passed date.
1197
     *
1198
     * @param sqlTS an SQL expression of type time stamp.
1199
     * @return 1 for January through 12 for December.
1200
     */
1201
    public String getMonth(final String sqlTS) {
1202
        return "MONTH(" + sqlTS + ")";
1203
    }
1204
 
67 ilm 1205
    public final String getFormatTimestamp(final Timestamp ts, final boolean basic) {
142 ilm 1206
        return this.getFormatTimestamp(SQLType.getFromSyntax(this, Types.TIMESTAMP, 0).toString(ts), basic);
67 ilm 1207
    }
1208
 
17 ilm 1209
    /**
67 ilm 1210
     * Return the SQL function that format a time stamp to a complete representation. The
1211
     * {@link SimpleDateFormat format} is {@value #TS_EXTENDED_JAVA_FORMAT} : microseconds and no
1212
     * time zone (only format supported by all systems).
1213
     * <p>
1214
     * NOTE : from ISO 8601:2004(E) §4.2.2.4 the decimal sign is included even in basic format.
1215
     * </p>
1216
     *
1217
     * @param sqlTS an SQL expression of type time stamp.
1218
     * @param basic <code>true</code> if the format should be basic, i.e. with the minimum number of
1219
     *        characters ; <code>false</code> if additional separators must be added (more legible).
1220
     * @return the SQL needed to format the passed parameter.
1221
     */
1222
    public abstract String getFormatTimestamp(final String sqlTS, final boolean basic);
1223
 
142 ilm 1224
    /**
1225
     * Return the native format from the passed simple one. If names are required, the server locale
1226
     * must be set correctly or {@link #getFormatTimestampSimple(DBRoot, String, List, Locale)}
1227
     * should be used.
1228
     *
1229
     * @param simpleFormat a list of either {@link DateProp} or literal string to include.
1230
     * @param useServerLocale <code>true</code> to allow the server to format strings (e.g. month
1231
     *        names).
1232
     * @return the native format (to use with {@link #getFormatTimestamp(String, String)}).
1233
     * @throws IllegalArgumentException if <code>useServerLocale</code> is <code>false</code> and
1234
     *         <code>simpleFormat</code> contains some name format properties.
1235
     */
1236
    public final String getTimestampFormat(final List<String> simpleFormat, final boolean useServerLocale) throws IllegalArgumentException {
1237
        final StringBuilder res = new StringBuilder();
1238
        final StringBuilder literal = new StringBuilder();
1239
        for (final String s : simpleFormat) {
1240
            if (!useServerLocale && DateProp.LOCALE_SENSITIVE_INSTANCES.contains(s))
1241
                throw new IllegalArgumentException("passed locale sensitive property : " + s);
1242
            final String spec = this.dateSpecifiers.get(s);
1243
            if (spec == null) {
1244
                // we can't append multiple literal : 'foo' then 'bar' makes 'foo''bar' i.e.
1245
                // "foo'bar" instead of the wanted "foobar"
1246
                literal.append(s);
1247
            } else {
1248
                if (literal.length() > 0) {
1249
                    res.append(this.quoteForTimestampFormat(literal.toString()));
1250
                    literal.setLength(0);
1251
                }
1252
                res.append(spec);
1253
            }
1254
        }
1255
        if (literal.length() > 0) {
1256
            res.append(this.quoteForTimestampFormat(literal.toString()));
1257
        }
1258
        return quoteString(res.toString());
1259
    }
1260
 
1261
    public abstract String quoteForTimestampFormat(final String text);
1262
 
1263
    /**
1264
     * Return the SQL expression that format the passed time stamp.
1265
     *
1266
     * @param sqlTS an SQL expression of type time stamp.
1267
     * @param nativeFormat a SQL varchar for a native format to this syntax, e.g. obtained from
1268
     *        {@link #getTimestampFormat(DBRoot, List, boolean)} .
1269
     * @return the SQL needed to format the passed parameter, e.g. FORMATDATETIME(CURRENT_TIMESTAMP,
1270
     *         'yyyy').
1271
     */
1272
    public abstract String getFormatTimestamp(final String sqlTS, final String nativeFormat);
1273
 
1274
    public final String getFormatTimestampSimple(String sqlTS, List<String> format) {
1275
        return this.getFormatTimestampSimple(sqlTS, format, Locale.getDefault());
1276
    }
1277
 
1278
    static private final int[] CALENDAR_DAYS = { Calendar.SUNDAY, Calendar.MONDAY, Calendar.TUESDAY, Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY, Calendar.SATURDAY };
1279
 
1280
    /**
1281
     * Return the SQL expression that format the passed time stamp.
1282
     *
1283
     * @param sqlTS an SQL expression of type time stamp.
1284
     * @param simpleFormat a list of either {@link DateProp} or literal string to include, e.g. [
1285
     *        "year is ", {@link DateProp#YEAR}].
1286
     * @param l the locale to use, <code>null</code> meaning use the server.
1287
     * @return the SQL needed to format the passed parameter.
1288
     */
1289
    public final String getFormatTimestampSimple(String sqlTS, List<String> simpleFormat, final Locale l) {
1290
        final boolean useServerLocale = l == null;
1291
        if (!useServerLocale) {
1292
            final List<String> statements = new ArrayList<String>();
1293
            // minimize function calls
1294
            final List<String> nonLocalSensitive = new ArrayList<String>();
1295
            for (final String s : simpleFormat) {
1296
                if (!DateProp.LOCALE_SENSITIVE_INSTANCES.contains(s)) {
1297
                    nonLocalSensitive.add(s);
1298
                } else {
1299
                    if (!nonLocalSensitive.isEmpty()) {
1300
                        statements.add(this.getFormatTimestamp(sqlTS, this.getTimestampFormat(nonLocalSensitive, useServerLocale)));
1301
                        nonLocalSensitive.clear();
1302
                    }
1303
                    final StringBuilder sb = new StringBuilder(512);
1304
                    final DateFormatSymbols symbols = DateFormatSymbols.getInstance(l);
1305
                    if (s == DateProp.DAY_NAME_IN_WEEK) {
1306
                        final CaseBuilder caseBuilder = createCaseBuilder(this.getDayOfWeek(sqlTS));
1307
                        final String[] weekdays = symbols.getWeekdays();
1308
                        for (int j = 0; j < CALENDAR_DAYS.length; j++) {
1309
                            // SQL function begins at 1
1310
                            caseBuilder.addWhen(String.valueOf(j + 1), quoteString(weekdays[CALENDAR_DAYS[j]]));
1311
                        }
1312
                        caseBuilder.setElse(quoteString("unknown week day name"));
1313
                        caseBuilder.build(sb);
1314
                    } else if (s == DateProp.MONTH_NAME) {
1315
                        final CaseBuilder caseBuilder = createCaseBuilder(this.getMonth(sqlTS));
1316
                        int i = 1;
1317
                        for (final String m : symbols.getMonths()) {
1318
                            caseBuilder.addWhen(String.valueOf(i), quoteString(m));
1319
                            i++;
1320
                        }
1321
                        caseBuilder.setElse(quoteString("unknown month name"));
1322
                        caseBuilder.build(sb);
1323
                    } else {
1324
                        throw new IllegalStateException("Unknown prop : " + s);
1325
                    }
1326
                    statements.add(sb.toString());
1327
                }
1328
            }
1329
            if (!nonLocalSensitive.isEmpty()) {
1330
                statements.add(this.getFormatTimestamp(sqlTS, this.getTimestampFormat(nonLocalSensitive, useServerLocale)));
1331
            }
1332
            return CollectionUtils.join(statements, this.getConcatOp());
1333
        } else {
1334
            return this.getFormatTimestamp(sqlTS, this.getTimestampFormat(simpleFormat, useServerLocale));
1335
        }
1336
    }
1337
 
67 ilm 1338
    public final String getInsertOne(final SQLName tableName, final List<String> fields, String... values) {
1339
        return this.getInsertOne(tableName, fields, Arrays.asList(values));
1340
    }
1341
 
1342
    public final String getInsertOne(final SQLName tableName, final List<String> fields, final List<String> values) {
1343
        return getInsert(tableName, fields, Collections.singletonList(values));
1344
    }
1345
 
1346
    public final String getInsert(final SQLName tableName, final List<String> fields, final List<List<String>> values) {
1347
        return "INSERT INTO " + tableName.quote() + "(" + quoteIdentifiers(fields) + ") " + getValues(values, fields.size());
1348
    }
1349
 
1350
    public final String getValues(final List<List<String>> rows) {
1351
        return this.getValues(rows, -1);
1352
    }
1353
 
1354
    /**
1355
     * Create a VALUES expression.
1356
     *
1357
     * @param rows the rows with the SQL expression for each cell.
1358
     * @param colCount the number of columns the rows must have, -1 meaning infer it from
1359
     *        <code>rows</code>.
1360
     * @return the VALUES expression, e.g. "VALUES (1, 'one'), (2, 'two'), (3, 'three')".
1361
     */
1362
    public final String getValues(final List<List<String>> rows, int colCount) {
1363
        final int rowCount = rows.size();
1364
        if (rowCount < 1)
1365
            throw new IllegalArgumentException("Empty rows will cause a syntax error");
1366
        if (colCount < 0)
1367
            colCount = rows.get(0).size();
1368
        final StringBuilder sb = new StringBuilder(rowCount * 64);
1369
        final char space = rowCount > 6 ? '\n' : ' ';
1370
        sb.append("VALUES");
1371
        sb.append(space);
1372
        for (final List<String> row : rows) {
1373
            if (row.size() != colCount)
1374
                throw new IllegalArgumentException("Row have wrong size, not " + colCount + " : " + row);
1375
            sb.append("(");
1376
            sb.append(CollectionUtils.join(row, ", "));
1377
            sb.append("),");
1378
            sb.append(space);
1379
        }
1380
        // remove last ", "
1381
        sb.setLength(sb.length() - 2);
1382
        return sb.toString();
1383
    }
1384
 
142 ilm 1385
    public final String getConstantTableStatement(final List<List<String>> rows) {
1386
        return this.getConstantTableStatement(rows, -1);
1387
    }
1388
 
67 ilm 1389
    /**
142 ilm 1390
     * Return a complete statement to return the passed list of rows.
1391
     *
1392
     * @param rows the rows with the SQL expression for each cell.
1393
     * @param colCount the number of columns the rows must have, -1 meaning infer it from
1394
     *        <code>rows</code>.
1395
     * @return the complete SQL expression that can be executed as is.
1396
     * @see #getValues(List, int)
1397
     * @see #getConstantTable(List, String, List)
1398
     */
1399
    public String getConstantTableStatement(final List<List<String>> rows, int colCount) {
1400
        return this.getValues(rows, colCount);
1401
    }
1402
 
1403
    /**
67 ilm 1404
     * Get a constant table usable as a join.
1405
     *
1406
     * @param rows the SQL values for the table, e.g. [["1", "'one'"], ["2", "'two'"]].
1407
     * @param alias the table alias, e.g. "t".
1408
     * @param columnsAlias the columns aliases.
1409
     * @return a constant table, e.g. ( VALUES (1, 'one'), (2, 'two') ) as "t" ("n", "name").
1410
     */
1411
    public String getConstantTable(final List<List<String>> rows, final String alias, final List<String> columnsAlias) {
1412
        final int colSize = columnsAlias.size();
1413
        if (colSize < 1)
1414
            throw new IllegalArgumentException("Empty columns will cause a syntax error");
1415
        final StringBuilder sb = new StringBuilder(rows.size() * 64);
1416
        sb.append("( ");
1417
        sb.append(getValues(rows, colSize));
1418
        sb.append(" ) as ");
1419
        sb.append(SQLBase.quoteIdentifier(alias));
1420
        sb.append(" (");
1421
        for (final String colAlias : columnsAlias) {
1422
            sb.append(SQLBase.quoteIdentifier(colAlias));
1423
            sb.append(", ");
1424
        }
1425
        // remove last ", "
1426
        sb.setLength(sb.length() - 2);
1427
        sb.append(")");
1428
        return sb.toString();
1429
    }
1430
 
142 ilm 1431
    protected final String getTablesMapJoin(final TablesMap tables, final String schemaExpr, final String tableExpr) {
67 ilm 1432
        final List<List<String>> rows = new ArrayList<List<String>>();
1433
        for (final Entry<String, Set<String>> e : tables.entrySet()) {
142 ilm 1434
            final String schemaName = this.quoteString(e.getKey());
67 ilm 1435
            if (e.getValue() == null) {
1436
                rows.add(Arrays.asList(schemaName, "NULL"));
1437
            } else {
1438
                for (final String tableName : e.getValue())
142 ilm 1439
                    rows.add(Arrays.asList(schemaName, this.quoteString(tableName)));
67 ilm 1440
            }
1441
        }
1442
        final String tableAlias = "tables";
1443
        final SQLName schemaName = new SQLName(tableAlias, "schema");
1444
        final SQLName tableName = new SQLName(tableAlias, "table");
1445
 
1446
        final String schemaWhere = schemaExpr + " = " + schemaName.quote();
1447
        final String tableWhere = "(" + tableName.quote() + " is null or " + tableExpr + " = " + tableName.quote() + ")";
1448
        return "INNER JOIN " + getConstantTable(rows, tableAlias, Arrays.asList(schemaName.getName(), tableName.getName())) + " on " + schemaWhere + " and " + tableWhere;
1449
    }
1450
 
1451
    /**
17 ilm 1452
     * A query to retrieve columns metadata from INFORMATION_SCHEMA. The result must have at least
1453
     * {@link #INFO_SCHEMA_NAMES_KEYS}.
1454
     *
1455
     * @param b the base.
67 ilm 1456
     * @param tables the tables by schemas names.
17 ilm 1457
     * @return the query to retrieve information about columns.
1458
     */
67 ilm 1459
    public abstract String getColumnsQuery(SQLBase b, TablesMap tables);
17 ilm 1460
 
1461
    /**
1462
     * Return the query to find the functions. The result must have 3 columns : schema, name and src
1463
     * (this should provide the most information possible, eg just the body, the complete SQL or
1464
     * <code>null</code> if nothing can be found).
1465
     *
1466
     * @param b the base.
1467
     * @param schemas the schemas we're interested in.
1468
     * @return the query or <code>null</code> if no information can be retrieved.
1469
     */
1470
    public abstract String getFunctionQuery(SQLBase b, Set<String> schemas);
1471
 
1472
    /**
41 ilm 1473
     * Return the constraints in the passed tables.
17 ilm 1474
     *
1475
     * @param b the base.
67 ilm 1476
     * @param tables the tables by schemas names.
17 ilm 1477
     * @return a list of map with at least "TABLE_SCHEMA", "TABLE_NAME", "CONSTRAINT_NAME",
83 ilm 1478
     *         "CONSTRAINT_TYPE", (List of String)"COLUMN_NAMES" keys and "DEFINITION".
17 ilm 1479
     * @throws SQLException if an error occurs.
1480
     */
67 ilm 1481
    public abstract List<Map<String, Object>> getConstraints(SQLBase b, TablesMap tables) throws SQLException;
17 ilm 1482
 
142 ilm 1483
    protected final String quoteStrings(Collection<String> c) {
17 ilm 1484
        return CollectionUtils.join(c, ", ", new ITransformer<String, String>() {
1485
            @Override
1486
            public String transformChecked(String s) {
142 ilm 1487
                return quoteString(s);
17 ilm 1488
            }
1489
        });
1490
    }
1491
 
1492
    public static final String quoteIdentifiers(Collection<String> c) {
1493
        return join(c, ", ", new ITransformer<String, String>() {
1494
            @Override
1495
            public String transformChecked(String s) {
1496
                return SQLBase.quoteIdentifier(s);
1497
            }
1498
        });
1499
    }
1500
 
1501
    public static final String getSchemaUniqueName(final String tableName, final String name) {
1502
        return name.startsWith(tableName) ? name : tableName + "_" + name;
1503
    }
1504
 
1505
    /**
1506
     * A query to retrieve triggers in the passed schemas and tables. The result must have at least
83 ilm 1507
     * TRIGGER_NAME, TABLE_SCHEMA, TABLE_NAME, ACTION (system dependent, e.g. "NEW.F = true") and
1508
     * SQL (the SQL needed to create the trigger, can be <code>null</code>).
17 ilm 1509
     *
1510
     * @param b the base.
67 ilm 1511
     * @param tables the tables by schemas names.
17 ilm 1512
     * @return the query to retrieve triggers.
1513
     * @throws SQLException if an error occurs.
1514
     */
67 ilm 1515
    public abstract String getTriggerQuery(SQLBase b, TablesMap tables) throws SQLException;
17 ilm 1516
 
1517
    public abstract String getDropTrigger(Trigger t);
1518
 
1519
    /**
1520
     * The part of an UPDATE query specifying tables and fields to update.
1521
     *
1522
     * @param t the table whose fields will change.
1523
     * @param tables the other tables of the update.
1524
     * @param setPart the fields of <code>t</code> and their values.
1525
     * @return the SQL specifying how to set the fields.
83 ilm 1526
     * @throws UnsupportedOperationException if this system doesn't support the passed update, e.g.
17 ilm 1527
     *         multi-table.
1528
     */
1529
    public String getUpdate(SQLTable t, List<String> tables, Map<String, String> setPart) throws UnsupportedOperationException {
83 ilm 1530
        String res = t.getSQLName().quote() + " SET\n" + CollectionUtils.join(setPart.entrySet(), ",\n", new ITransformer<Entry<String, String>, String>() {
17 ilm 1531
            @Override
1532
            public String transformChecked(Entry<String, String> input) {
83 ilm 1533
                // pg require that fields are unprefixed
1534
                return SQLBase.quoteIdentifier(input.getKey()) + " = " + input.getValue();
17 ilm 1535
            }
1536
        });
83 ilm 1537
        if (tables.size() > 0)
1538
            res += " FROM " + CollectionUtils.join(tables, ", ");
1539
        return res;
17 ilm 1540
    }
1541
 
144 ilm 1542
    public DeferredClause getSetTableComment(final String comment) {
1543
        return new DeferredClause() {
17 ilm 1544
            @Override
1545
            public ClauseType getType() {
1546
                return ClauseType.OTHER;
1547
            }
1548
 
1549
            @Override
144 ilm 1550
            protected String asString(ChangeTable<?> ct, SQLName tableName) {
1551
                return "COMMENT ON TABLE " + tableName.quote() + " IS " + ct.getSyntax().quoteString(comment) + ";";
17 ilm 1552
            }
1553
        };
1554
    }
182 ilm 1555
 
1556
    /**
1557
     * The expression that returns the current session.
1558
     *
1559
     * @return the expression to know the current session ID.
1560
     * @see #getSessionsQuery(DBSystemRoot, boolean)
1561
     */
1562
    public abstract String getSessionIDExpression();
1563
 
1564
    public final String getSessionsQuery(final DBSystemRoot sysRoot) {
1565
        return this.getSessionsQuery(sysRoot, true);
1566
    }
1567
 
1568
    /**
1569
     * Return a query to list the sessions in the passed system root.
1570
     *
1571
     * @param sysRoot the system root.
1572
     * @param includeSelf <code>true</code> if the session executing the query should be returned.
1573
     * @return a query returning ID, QUERY and USER_NAME columns.
1574
     */
1575
    public abstract String getSessionsQuery(final DBSystemRoot sysRoot, final boolean includeSelf);
1576
 
1577
    /**
1578
     * Return a query to grant privileges on a table.
1579
     *
1580
     * @param privileges which privileges, <code>null</code> for ALL, e.g. "SELECT".
1581
     * @param tableName which table.
1582
     * @param role which role, <code>null</code> for everyone, e.g. 'joe'.
1583
     * @return the query.
1584
     */
1585
    public String getGrantQuery(final List<String> privileges, final SQLName tableName, final String role) {
1586
        final String priv = privileges == null ? "ALL" : CollectionUtils.join(privileges, ", ");
1587
        return "GRANT " + priv + " ON " + tableName + " TO " + (role == null ? getAllUsersForGrant() : role);
1588
    }
1589
 
1590
    protected String getAllUsersForGrant() {
1591
        return "PUBLIC";
1592
    }
1593
 
1594
    public String getVersionFunction() {
1595
        return "version()";
1596
    }
1597
 
1598
    /**
1599
     * Return a query to allow/disallow connections to the passed database. I.e. allow exclusive
1600
     * access to the database.
1601
     *
1602
     * @param sysRootName the database name.
1603
     * @param allow <code>true</code> to allow connections, <code>false</code> to disallow.
1604
     * @return the query, <code>null</code> if not supported.
1605
     */
1606
    public String getAllowConnectionsQuery(String sysRootName, final boolean allow) {
1607
        return null;
1608
    }
1609
 
1610
    public boolean isConnectionDisallowedException(final SQLException exn) {
1611
        throw new UnsupportedOperationException();
1612
    }
1613
 
1614
    public final String getSQLArray(final List<String> sqlExpressions) {
1615
        return this.getSQLArray(sqlExpressions, null);
1616
    }
1617
 
1618
    /**
1619
     * Return an SQL expression of an array.
1620
     *
1621
     * @param sqlExpressions the items.
1622
     * @param type the component type, can be <code>null</code> to infer it from items (i.e. when
1623
     *        <code>sqlExpressions</code> isn't empty).
1624
     * @return an SQL expression.
1625
     */
1626
    public String getSQLArray(final List<String> sqlExpressions, final String type) {
1627
        throw new UnsupportedOperationException();
1628
    }
1629
 
1630
    public String getSQLArrayContains(final String arrayExpression, final String itemExpression) {
1631
        throw new UnsupportedOperationException();
1632
    }
1633
 
1634
    public String getSQLArrayLength(final String arrayExpression) {
1635
        throw new UnsupportedOperationException();
1636
    }
1637
 
1638
    public String getSQLArrayConcat(final String arrayExpression, final String array2Expression) {
1639
        throw new UnsupportedOperationException();
1640
    }
1641
 
1642
    public String getSQLArrayAppend(final String arrayExpression, final String itemExpression) {
1643
        throw new UnsupportedOperationException();
1644
    }
1645
 
1646
    public String getSQLArraySlice(final String arrayExpression, final String index1Expression, final String index2Expression) {
1647
        throw new UnsupportedOperationException();
1648
    }
17 ilm 1649
}