OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 144 | 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.utils;
15
 
16
import static java.util.Collections.singletonList;
142 ilm 17
 
83 ilm 18
import org.openconcerto.sql.Log;
17 ilm 19
import org.openconcerto.sql.model.SQLBase;
20
import org.openconcerto.sql.model.SQLField;
21
import org.openconcerto.sql.model.SQLName;
22
import org.openconcerto.sql.model.SQLSyntax;
23
import org.openconcerto.sql.model.SQLSystem;
24
import org.openconcerto.sql.model.SQLTable;
25
import org.openconcerto.sql.model.SQLTable.Index;
83 ilm 26
import org.openconcerto.sql.model.SQLTable.SQLIndex;
80 ilm 27
import org.openconcerto.sql.model.SQLType;
83 ilm 28
import org.openconcerto.sql.model.Where;
17 ilm 29
import org.openconcerto.sql.model.graph.Link;
57 ilm 30
import org.openconcerto.sql.model.graph.Link.Rule;
20 ilm 31
import org.openconcerto.sql.model.graph.SQLKey;
17 ilm 32
import org.openconcerto.utils.CollectionUtils;
83 ilm 33
import org.openconcerto.utils.ListMap;
17 ilm 34
import org.openconcerto.utils.ReflectUtils;
83 ilm 35
import org.openconcerto.utils.StringUtils;
17 ilm 36
import org.openconcerto.utils.cc.ITransformer;
37
 
80 ilm 38
import java.math.BigDecimal;
17 ilm 39
import java.util.ArrayList;
83 ilm 40
import java.util.Arrays;
17 ilm 41
import java.util.Collection;
42
import java.util.Collections;
19 ilm 43
import java.util.EnumSet;
44
import java.util.LinkedHashSet;
17 ilm 45
import java.util.List;
144 ilm 46
import java.util.Objects;
17 ilm 47
import java.util.Set;
83 ilm 48
import java.util.regex.Pattern;
17 ilm 49
 
144 ilm 50
import org.h2.api.Trigger;
51
 
17 ilm 52
/**
53
 * Construct a statement about a table.
54
 *
55
 * @author Sylvain
56
 * @param <T> type of this
57
 * @see AlterTable
58
 * @see SQLCreateTable
59
 */
60
public abstract class ChangeTable<T extends ChangeTable<T>> {
61
 
83 ilm 62
    private static final String TRIGGER_SUFFIX = "_trigger";
63
    protected static final String[] TRIGGER_EVENTS = { "INSERT", "UPDATE" };
64
 
65
    // group 1 is the columns, group 2 the where
66
    public static final Pattern H2_UNIQUE_TRIGGER_PATTERN = Pattern.compile("\\snew " + PartialUniqueTrigger.class.getName() + "\\(\\s*java.util.Arrays.asList\\((.+)\\)\\s*,(.+)\\)");
144 ilm 67
    // group 1 is the class name, e.g. CALL
68
    // "org.openconcerto.sql.utils.ChangeTableTest$Test_PartialUniqueTrigger"
69
    public static final String H2_UNIQUE_TRIGGER_CLASS_SUFFIX = "_" + PartialUniqueTrigger.class.getSimpleName();
70
    public static final Pattern H2_UNIQUE_TRIGGER_CLASS_PATTERN = Pattern.compile("CALL\\s+\"(.*" + Pattern.quote(H2_UNIQUE_TRIGGER_CLASS_SUFFIX) + ")\"");
83 ilm 71
    public static final Pattern H2_LIST_PATTERN = Pattern.compile("\\s*,\\s*");
72
 
73
    public static final String MYSQL_TRIGGER_SUFFIX_1 = getTriggerSuffix(TRIGGER_EVENTS[0]);
74
    public static final String MYSQL_TRIGGER_SUFFIX_2 = getTriggerSuffix(TRIGGER_EVENTS[1]);
75
    public static final String MYSQL_FAKE_PROCEDURE = "Unique constraint violation";
76
    public static final String MYSQL_TRIGGER_EXCEPTION = "call " + SQLBase.quoteIdentifier(MYSQL_FAKE_PROCEDURE);
77
    // group 1 is the table name, group 2 the where
142 ilm 78
    public static final Pattern MYSQL_UNIQUE_TRIGGER_PATTERN = Pattern.compile(
79
            "IF\\s*\\(\\s*" + Pattern.quote("SELECT COUNT(*)") + "\\s+FROM\\s+(.+)\\s+where\\s+(.+)\\)\\s*>\\s*1\\s+then\\s+" + Pattern.quote(MYSQL_TRIGGER_EXCEPTION), Pattern.CASE_INSENSITIVE);
83 ilm 80
    // to split the where
81
    public static final Pattern MYSQL_WHERE_PATTERN = Pattern.compile("\\s+and\\s+", Pattern.CASE_INSENSITIVE);
82
    // to find the column name
83
    public static final Pattern MYSQL_WHERE_EQ_PATTERN = Pattern.compile("(NEW.)?(.+)\\s*=\\s*(NEW.)?\\2");
84
 
85
    public static final String getIndexName(final String triggerName, final SQLSystem system) {
86
        if (system == SQLSystem.MYSQL && triggerName.endsWith(MYSQL_TRIGGER_SUFFIX_1)) {
87
            return triggerName.substring(0, triggerName.length() - MYSQL_TRIGGER_SUFFIX_1.length());
88
        } else if (system == SQLSystem.H2 && triggerName.endsWith(TRIGGER_SUFFIX)) {
89
            return triggerName.substring(0, triggerName.length() - TRIGGER_SUFFIX.length());
90
        } else {
91
            return null;
92
        }
93
    }
94
 
95
    static private String getTriggerSuffix(final String event) {
96
        return (event == null ? "" : '_' + event.toLowerCase()) + TRIGGER_SUFFIX;
97
    }
98
 
17 ilm 99
    public static enum ClauseType {
144 ilm 100
        ADD_COL, ADD_CONSTRAINT, ADD_INDEX, DROP_COL, DROP_CONSTRAINT, DROP_INDEX, ALTER_COL,
101
        /**
102
         * e.g. SET COMMENT.
103
         */
104
        OTHER,
105
        /**
106
         * e.g. DROP TRIGGER.
107
         */
108
        OTHER_DROP,
109
        /**
110
         * e.g. CREATE TRIGGER.
111
         */
112
        OTHER_ADD;
17 ilm 113
    }
114
 
41 ilm 115
    public static enum ConcatStep {
144 ilm 116
        // *_CONSTRAINT must be steps to allow to insert rows in different tables that references
117
        // each other.
118
        // *_INDEX should be steps to avoid constantly updating the index when inserting many rows.
119
 
17 ilm 120
        // drop constraints first since, at least in pg, they depend on indexes
142 ilm 121
        DROP_CONSTRAINT(ClauseType.DROP_CONSTRAINT),
17 ilm 122
        // drop indexes before columns to avoid having to know if the index is dropped because its
123
        // columns are dropped
144 ilm 124
        DROP_INDEX(ClauseType.DROP_INDEX),
125
        // drop first to allow to drop and re-create, DROP_COL last of DROP clauses since a column
126
        // doesn't depend on anything (likewise ADD_COL first of ADD clauses), ALTER_COL & OTHER at
127
        // the end to allow to use new fields
128
        ALTER_TABLE(ClauseType.OTHER_DROP, ClauseType.DROP_COL, ClauseType.ADD_COL, ClauseType.OTHER_ADD, ClauseType.ALTER_COL, ClauseType.OTHER),
17 ilm 129
        // likewise add indexes before since constraints need them
142 ilm 130
        ADD_INDEX(ClauseType.ADD_INDEX), ADD_CONSTRAINT(ClauseType.ADD_CONSTRAINT);
17 ilm 131
 
132
        private final Set<ClauseType> types;
133
 
134
        private ConcatStep(ClauseType... types) {
144 ilm 135
            this.types = new LinkedHashSet<ClauseType>();
17 ilm 136
            for (final ClauseType t : types)
137
                this.types.add(t);
138
        }
139
 
140
        public final Set<ClauseType> getTypes() {
141
            return this.types;
142
        }
143
    }
144
 
67 ilm 145
    /**
146
     * Allow to change names of tables.
147
     *
148
     * @author Sylvain
149
     */
150
    public static class NameTransformer {
151
 
152
        /**
153
         * Transformer that does nothing.
154
         */
155
        public static final NameTransformer NOP = new NameTransformer();
156
 
157
        /**
158
         * Called once for each {@link ChangeTable}.
159
         *
160
         * @param tableName the original table name.
161
         * @return the name that will be used.
162
         */
163
        public SQLName transformTableName(final SQLName tableName) {
164
            return tableName;
165
        }
166
 
167
        /**
168
         * Called once for each foreign key.
169
         *
170
         * @param rootName the name of the root of the table.
171
         * @param tableName the name of the table.
172
         * @param linkDest the name of the destination table.
173
         * @return the name that will be used to reference the foreign table.
174
         */
175
        public SQLName transformLinkDestTableName(final String rootName, final String tableName, final SQLName linkDest) {
176
            return transformTableName(linkDest.getItemCount() == 1 ? new SQLName(rootName, linkDest.getName()) : linkDest);
177
        }
178
    }
179
 
180
    public static class ChangeRootNameTransformer extends NameTransformer {
181
 
182
        private final String r;
183
 
184
        public ChangeRootNameTransformer(String r) {
185
            super();
186
            this.r = r;
187
        }
188
 
189
        @Override
190
        public SQLName transformTableName(final SQLName tableName) {
191
            return new SQLName(this.r, tableName.getName());
192
        }
193
 
194
        @Override
195
        public SQLName transformLinkDestTableName(final String rootName, final String tableName, final SQLName linkDest) {
196
            return linkDest.getItemCount() == 1 ? transformTableName(new SQLName(rootName, linkDest.getName())) : linkDest;
197
        }
198
    }
199
 
19 ilm 200
    public static final Set<ClauseType> ORDERED_TYPES;
201
 
202
    static {
203
        final Set<ClauseType> tmp = new LinkedHashSet<ClauseType>(ClauseType.values().length);
204
        for (final ConcatStep step : ConcatStep.values())
205
            tmp.addAll(step.getTypes());
206
        assert tmp.equals(EnumSet.allOf(ClauseType.class)) : "ConcatStep is missing some types : " + tmp;
207
        ORDERED_TYPES = Collections.unmodifiableSet(tmp);
208
    }
209
 
17 ilm 210
    /**
211
     * Compute the SQL needed to create all passed tables, handling foreign key cycles.
212
     *
213
     * @param cts the tables to create.
214
     * @param r where to create them.
215
     * @return the SQL needed.
216
     */
19 ilm 217
    public static List<String> cat(List<? extends ChangeTable<?>> cts, final String r) {
67 ilm 218
        return cat(cts, new ChangeRootNameTransformer(r));
17 ilm 219
    }
220
 
67 ilm 221
    public static List<String> cat(final List<? extends ChangeTable<?>> cts) {
222
        return cat(cts, NameTransformer.NOP);
223
    }
224
 
225
    public static List<String> cat(final List<? extends ChangeTable<?>> cts, final NameTransformer transf) {
226
        return cat(cts, transf, false);
227
    }
228
 
41 ilm 229
    /**
230
     * Compute the SQL needed to create all passed tables split at the passed boundaries. E.g. if
231
     * you wanted to create tables without constraints, insert some data and then add constraints,
232
     * you would pass <code>EnumSet.of(ConcatStep.ADD_CONSTRAINT)</code>.
233
     *
234
     * @param cts the tables to create.
235
     * @param r where to create them.
236
     * @param boundaries where to split the SQL statements.
237
     * @return the SQL needed, by definition the list size is one more than <code>boundaries</code>
238
     *         size, e.g. if no boundaries are passed all SQL will be in one list.
239
     */
240
    public static List<List<String>> cat(final Collection<? extends ChangeTable<?>> cts, final String r, final EnumSet<ConcatStep> boundaries) {
67 ilm 241
        if (r == null)
242
            throw new NullPointerException("r is null");
243
        return cat(cts, new ChangeRootNameTransformer(r), boundaries);
244
    }
245
 
246
    public static List<List<String>> cat(final Collection<? extends ChangeTable<?>> cts, final NameTransformer transf, final EnumSet<ConcatStep> boundaries) {
41 ilm 247
        final List<List<String>> res = new ArrayList<List<String>>();
248
        List<String> current = null;
17 ilm 249
        for (final ConcatStep step : ConcatStep.values()) {
41 ilm 250
            if (current == null || boundaries.contains(step)) {
251
                current = new ArrayList<String>();
252
                res.add(current);
253
            }
17 ilm 254
            for (final ChangeTable<?> ct : cts) {
67 ilm 255
                final String asString = ct.asString(transf, step);
17 ilm 256
                if (asString != null && asString.length() > 0) {
41 ilm 257
                    current.add(asString);
17 ilm 258
                }
259
            }
41 ilm 260
 
17 ilm 261
        }
41 ilm 262
        assert res.size() == boundaries.size() + 1;
263
        return res;
264
    }
265
 
67 ilm 266
    private static List<String> cat(List<? extends ChangeTable<?>> cts, final NameTransformer transf, final boolean forceCat) {
267
        final List<String> res = cat(cts, transf, EnumSet.noneOf(ConcatStep.class)).get(0);
19 ilm 268
        // don't return [""] because the caller might test the size of the result and assume that
269
        // the DB was changed
17 ilm 270
        // MySQL needs to have its "alter table add/drop fk" in separate execute()
271
        // (multiple add would work in 5.0)
19 ilm 272
        if (!forceCat && (cts.size() == 0 || cts.get(0).getSyntax().getSystem() == SQLSystem.MYSQL))
17 ilm 273
            return res;
274
        else
275
            return Collections.singletonList(CollectionUtils.join(res, "\n"));
276
    }
277
 
19 ilm 278
    public static String catToString(List<? extends ChangeTable<?>> cts, final String r) {
67 ilm 279
        return cat(cts, new ChangeRootNameTransformer(r), true).get(0);
17 ilm 280
    }
281
 
182 ilm 282
    static public String getForeignColumDefaultValue(final SQLTable foreignTable) {
283
        return foreignTable.getKey().getType().toString(foreignTable.getUndefinedIDNumber());
284
    }
285
 
80 ilm 286
    // allow to factor column name from table and FCSpec
287
    public static final class ForeignColSpec {
288
 
289
        static public ForeignColSpec fromCreateTable(SQLCreateTableBase<?> createTable) {
290
            final List<String> primaryKey = createTable.getPrimaryKey();
291
            if (primaryKey.size() != 1)
292
                throw new IllegalArgumentException("Not exactly one field in the foreign primary key : " + primaryKey);
93 ilm 293
            return new ForeignColSpec(null, new SQLName(createTable.getRootName(), createTable.getName()), primaryKey.get(0), null);
80 ilm 294
        }
295
 
296
        static public ForeignColSpec fromTable(SQLTable foreignTable) {
297
            return fromTable(foreignTable, true);
298
        }
299
 
300
        static public ForeignColSpec fromTable(SQLTable foreignTable, final boolean absolute) {
301
            if (foreignTable == null)
302
                throw new NullPointerException("null table");
182 ilm 303
            final String defaultVal = getForeignColumDefaultValue(foreignTable);
80 ilm 304
            final SQLName n = absolute ? foreignTable.getSQLName() : new SQLName(foreignTable.getName());
305
            return new ForeignColSpec(null, n, foreignTable.getKey().getName(), defaultVal);
306
        }
307
 
308
        private String fk;
309
        private final SQLName table;
310
        private final String pk;
311
        private final String defaultVal;
312
 
313
        public ForeignColSpec(String fk, SQLName table, String pk, String defaultVal) {
314
            super();
315
            this.table = table;
316
            this.setColumnName(fk);
317
            this.pk = pk;
318
            this.defaultVal = defaultVal;
319
        }
320
 
321
        public final ForeignColSpec setColumnNameFromTable() {
322
            return this.setColumnNameWithSuffix("");
323
        }
324
 
325
        public final ForeignColSpec setColumnNameWithSuffix(final String suffix) {
326
            return this.setColumnName(SQLKey.PREFIX + getTable().getName() + (suffix.length() == 0 ? "" : "_" + suffix));
327
        }
328
 
329
        public final ForeignColSpec setColumnName(final String fk) {
330
            if (fk == null)
331
                this.setColumnNameFromTable();
332
            else
333
                this.fk = fk;
334
            return this;
335
        }
336
 
337
        public final String getColumnName() {
338
            return this.fk;
339
        }
340
 
341
        public final SQLName getTable() {
342
            return this.table;
343
        }
344
 
345
        public final String getPrimaryKeyName() {
346
            return this.pk;
347
        }
348
 
349
        public final String getDefaultVal() {
350
            return this.defaultVal;
351
        }
352
 
353
        public final FCSpec createFCSpec(final Rule updateRule, final Rule deleteRule) {
354
            return new FCSpec(Collections.singletonList(this.getColumnName()), this.getTable(), Collections.singletonList(this.getPrimaryKeyName()), updateRule, deleteRule);
355
        }
356
    }
357
 
41 ilm 358
    public static final class FCSpec {
67 ilm 359
 
360
        static public FCSpec createFromLink(final Link l) {
361
            return createFromLink(l, l.getTarget());
362
        }
363
 
364
        /**
365
         * Create an instance using an existing link but pointing to another table.
366
         *
367
         * @param l an existing link, e.g. root1.LOCAL pointing to root1.BATIMENT.
368
         * @param newDest the new destination for the link, e.g. root2.BATIMENT.
369
         * @return a new instance, e.g. root1.LOCAL pointing to root2.BATIMENT.
142 ilm 370
         * @throws IllegalArgumentException if <code>newDest</code> is not compatible with <code>l.
371
         *         {@link Link#getTarget() getTarget()}</code>.
67 ilm 372
         */
373
        static public FCSpec createFromLink(final Link l, final SQLTable newDest) {
374
            if (newDest != l.getTarget()) {
375
                final List<SQLField> ffs = l.getFields();
376
                final Set<SQLField> pks = newDest.getPrimaryKeys();
377
                if (ffs.size() != pks.size())
378
                    throw new IllegalArgumentException("Size mismatch : " + ffs + " " + pks);
379
                int i = 0;
380
                for (final SQLField pk : pks) {
381
                    if (!ffs.get(i).getType().equals(pk.getType()))
382
                        throw new IllegalArgumentException("Type mismatch " + ffs.get(i) + " " + pk);
383
                    i++;
384
                }
385
            }
386
            return new FCSpec(l.getCols(), newDest.getContextualSQLName(l.getSource()), newDest.getPKsNames(), l.getUpdateRule(), l.getDeleteRule());
387
        }
388
 
41 ilm 389
        private final List<String> cols;
390
        private final SQLName refTable;
391
        private final List<String> refCols;
57 ilm 392
        private final Rule updateRule, deleteRule;
41 ilm 393
 
57 ilm 394
        public FCSpec(List<String> cols, SQLName refTable, List<String> refCols, final Rule updateRule, final Rule deleteRule) {
41 ilm 395
            super();
396
            if (refTable.getItemCount() == 0)
397
                throw new IllegalArgumentException(refTable + " is empty.");
398
            this.cols = Collections.unmodifiableList(new ArrayList<String>(cols));
399
            this.refTable = refTable;
400
            this.refCols = Collections.unmodifiableList(new ArrayList<String>(refCols));
57 ilm 401
            this.updateRule = updateRule;
402
            this.deleteRule = deleteRule;
41 ilm 403
        }
404
 
405
        public final List<String> getCols() {
406
            return this.cols;
407
        }
408
 
409
        public final SQLName getRefTable() {
410
            return this.refTable;
411
        }
412
 
413
        public final List<String> getRefCols() {
414
            return this.refCols;
415
        }
57 ilm 416
 
417
        public final Rule getUpdateRule() {
418
            return this.updateRule;
419
        }
420
 
421
        public final Rule getDeleteRule() {
422
            return this.deleteRule;
423
        }
41 ilm 424
    }
425
 
144 ilm 426
    static protected final class Clauses {
427
        private final ListMap<ClauseType, DeferredGeneralClause> clauses;
428
 
429
        Clauses() {
430
            this.clauses = new ListMap<>();
431
        }
432
 
433
        Clauses(final Clauses clauses) {
434
            this.clauses = new ListMap<>(clauses.clauses);
435
        }
436
 
437
        public void reset() {
438
            this.clauses.clear();
439
        }
440
 
441
        public final void addClause(final String s, final ClauseType type) {
442
            this.addClause(-1, s, type);
443
        }
444
 
445
        public final void addClause(final int index, final String s, final ClauseType type) {
446
            this.addClause(index, new DeferredGeneralClause() {
447
                @Override
448
                public ClauseType getType() {
449
                    return type;
450
                }
451
 
452
                @Override
453
                public String asString(ChangeTable<?> ct, SQLName tableName, NameTransformer transf) {
454
                    return s;
455
                }
456
            });
457
        }
458
 
459
        public final void addClause(final DeferredGeneralClause s) {
460
            this.addClause(-1, s);
461
        }
462
 
463
        public final void addClause(final int index, final DeferredGeneralClause s) {
464
            if (s != null) {
465
                if (index < 0) {
466
                    this.clauses.add(s.getType(), s);
467
                } else {
468
                    this.clauses.addAll(s.getType(), Collections.<DeferredGeneralClause> emptyList());
469
                    this.clauses.get(s.getType()).add(index, s);
470
                }
471
            }
472
        }
473
 
474
        public final void addAllClauses(List<DeferredGeneralClause> clauses) {
475
            for (final DeferredGeneralClause c : clauses) {
476
                this.addClause(c);
477
            }
478
        }
479
 
480
        private final List<String> getClauses(final ChangeTable<?> ct, final SQLName tableName, final NameTransformer transf, final Set<ClauseType> types, final String intraTypeSep) {
481
            final ITransformer<DeferredGeneralClause, String> tf = new ITransformer<DeferredGeneralClause, String>() {
482
                @Override
483
                public String transformChecked(DeferredGeneralClause input) {
484
                    return input.asString(ct, tableName, transf);
485
                }
486
            };
487
            final List<String> res = new ArrayList<String>();
488
            for (final ClauseType type : ORDERED_TYPES) {
489
                if (types.contains(type)) {
490
                    final List<DeferredGeneralClause> clauses = this.clauses.getNonNull(type);
491
                    if (clauses.size() > 0) {
492
                        if (intraTypeSep == null) {
493
                            for (final DeferredGeneralClause c : clauses) {
494
                                res.add(tf.transformChecked(c));
495
                            }
496
                        } else {
497
                            res.add(CollectionUtils.join(clauses, intraTypeSep, tf));
498
                        }
499
                    }
500
                }
501
            }
502
            return res;
503
        }
504
 
505
        // all clauses, not grouped
506
        protected final List<String> getClauses(final ChangeTable<?> ct, final SQLName tableName, final NameTransformer transf, final Set<ClauseType> types) {
507
            return this.getClauses(ct, tableName, transf, types, null);
508
        }
509
 
510
        protected final void appendTo(final StringBuffer res, final ChangeTable<?> ct, final SQLName tableName, final NameTransformer transf, final Set<ClauseType> types) {
511
            final List<String> outClauses = this.getClauses(ct, tableName, transf, types);
512
            if (outClauses.size() > 0) {
513
                res.append("\n\n");
514
                res.append(CollectionUtils.join(outClauses, "\n"));
515
            }
516
        }
517
    }
518
 
519
    static protected final class InAndOutClauses {
520
        private final Clauses inClauses;
521
        private final Clauses outClauses;
522
 
523
        protected InAndOutClauses() {
524
            this.inClauses = new Clauses();
525
            this.outClauses = new Clauses();
526
        }
527
 
528
        protected InAndOutClauses(final InAndOutClauses c) {
529
            this.inClauses = new Clauses(c.inClauses);
530
            this.outClauses = new Clauses(c.outClauses);
531
        }
532
 
533
        public void reset() {
534
            this.inClauses.reset();
535
            this.outClauses.reset();
536
        }
537
 
538
        public boolean isEmpty() {
539
            return this.inClauses.clauses.isEmpty() && this.outClauses.clauses.isEmpty();
540
        }
541
 
542
        public Clauses getInClauses() {
543
            return this.inClauses;
544
        }
545
 
546
        public Clauses getOutClauses() {
547
            return this.outClauses;
548
        }
549
    }
550
 
67 ilm 551
    private String rootName, name;
17 ilm 552
    private final SQLSyntax syntax;
41 ilm 553
    private final List<FCSpec> fks;
144 ilm 554
    private final InAndOutClauses clauses;
17 ilm 555
 
67 ilm 556
    public ChangeTable(final SQLSyntax syntax, final String rootName, final String name) {
17 ilm 557
        super();
558
        this.syntax = syntax;
67 ilm 559
        this.rootName = rootName;
17 ilm 560
        this.name = name;
41 ilm 561
        this.fks = new ArrayList<FCSpec>();
144 ilm 562
        this.clauses = new InAndOutClauses();
17 ilm 563
 
564
        // check that (T) this; will succeed
565
        if (this.getClass() != ReflectUtils.getTypeArguments(this, ChangeTable.class).get(0))
566
            throw new IllegalStateException("illegal subclass: " + this.getClass());
567
    }
568
 
569
    @SuppressWarnings("unchecked")
570
    protected final T thisAsT() {
571
        return (T) this;
572
    }
573
 
574
    public final SQLSyntax getSyntax() {
575
        return this.syntax;
576
    }
577
 
578
    /**
579
     * Reset this instance's attributes to default values. Ie clauses will be emptied but if the
580
     * name was changed it won't be changed back to its original value (since it has no default
581
     * value).
582
     */
583
    public void reset() {
584
        this.fks.clear();
144 ilm 585
        this.clauses.reset();
17 ilm 586
    }
587
 
588
    public boolean isEmpty() {
144 ilm 589
        return this.fks.isEmpty() && this.clauses.isEmpty();
17 ilm 590
    }
591
 
592
    /**
593
     * Adds a varchar column not null and with '' as the default.
594
     *
595
     * @param name the name of the column.
596
     * @param count the number of char.
597
     * @return this.
83 ilm 598
     * @throws IllegalArgumentException if <code>count</code> is too high.
17 ilm 599
     */
600
    public final T addVarCharColumn(String name, int count) {
83 ilm 601
        return this.addVarCharColumn(name, count, false);
602
    }
603
 
604
    /**
605
     * Adds a varchar column not null and with '' as the default.
606
     *
607
     * @param name the name of the column.
608
     * @param count the number of characters.
142 ilm 609
     * @param lenient <code>true</code> if <code>count</code> should be restricted to the
610
     *        {@link SQLSyntax#getMaximumVarCharLength() maximum} allowed value of the system,
611
     *        <code>false</code> will throw an exception.
83 ilm 612
     * @return this.
613
     * @throws IllegalArgumentException if <code>count</code> is too high and <code>lenient</code>
614
     *         is <code>false</code>.
615
     */
616
    public final T addVarCharColumn(final String name, int count, final boolean lenient) throws IllegalArgumentException {
142 ilm 617
        return this.addVarCharColumn(name, count, lenient, "''", false);
618
    }
619
 
620
    /**
621
     * Adds a varchar column.
622
     *
623
     * @param name the name of the column.
624
     * @param count the number of characters.
625
     * @param lenient <code>true</code> if <code>count</code> should be restricted to the
626
     *        {@link SQLSyntax#getMaximumVarCharLength() maximum} allowed value of the system,
627
     *        <code>false</code> will throw an exception.
628
     * @param defaultValue the SQL default value of the column, can be <code>null</code>, e.g. "''".
629
     * @param nullable whether the column accepts NULL.
630
     * @return this.
631
     * @throws IllegalArgumentException if <code>count</code> is too high and <code>lenient</code>
632
     *         is <code>false</code>.
633
     */
634
    public final T addVarCharColumn(final String name, int count, final boolean lenient, final String defaultValue, final boolean nullable) throws IllegalArgumentException {
83 ilm 635
        final int max = getSyntax().getMaximumVarCharLength();
636
        if (count > max) {
637
            if (lenient) {
638
                Log.get().fine("Truncated " + name + " from " + count + " to " + max);
639
                count = max;
640
            } else {
641
                throw new IllegalArgumentException("Count too high : " + count + " > " + max);
642
            }
643
        }
142 ilm 644
        return this.addColumn(name, "varchar(" + count + ")", defaultValue, nullable);
17 ilm 645
    }
646
 
647
    public final T addDateAndTimeColumn(String name) {
648
        return this.addColumn(name, getSyntax().getDateAndTimeType());
649
    }
650
 
20 ilm 651
    /**
652
     * Adds a non-null integer column.
653
     *
654
     * @param name the name of the column.
655
     * @param defaultVal the default value of the column.
656
     * @return this.
657
     */
658
    public final T addIntegerColumn(String name, int defaultVal) {
659
        return this.addIntegerColumn(name, defaultVal, false);
660
    }
661
 
662
    /**
663
     * Adds an integer column.
664
     *
665
     * @param name the name of the column.
666
     * @param defaultVal the default value of the column, can be <code>null</code>.
667
     * @param nullable whether the column accepts NULL.
668
     * @return this.
669
     */
670
    public final T addIntegerColumn(String name, Integer defaultVal, boolean nullable) {
67 ilm 671
        return this.addNumberColumn(name, Integer.class, defaultVal, nullable);
20 ilm 672
    }
673
 
41 ilm 674
    public final T addLongColumn(String name, Long defaultVal, boolean nullable) {
67 ilm 675
        return this.addNumberColumn(name, Long.class, defaultVal, nullable);
41 ilm 676
    }
677
 
678
    public final T addShortColumn(String name, Short defaultVal, boolean nullable) {
67 ilm 679
        return this.addNumberColumn(name, Short.class, defaultVal, nullable);
41 ilm 680
    }
681
 
67 ilm 682
    /**
683
     * Adds a number column.
684
     *
685
     * @param name the name of the column.
686
     * @param javaType the java class, it must be supported by the {@link #getSyntax() syntax}, e.g.
687
     *        Double.class.
688
     * @param defaultVal the default value of the column, can be <code>null</code>, e.g. 3.14.
689
     * @param nullable whether the column accepts NULL.
690
     * @return this.
691
     * @see SQLSyntax#getTypeNames(Class)
692
     */
693
    public final <N extends Number> T addNumberColumn(String name, Class<N> javaType, N defaultVal, boolean nullable) {
83 ilm 694
        final Collection<String> typeNames = getSyntax().getTypeNames(javaType);
67 ilm 695
        if (typeNames.size() == 0)
696
            throw new IllegalArgumentException(javaType + " isn't supported by " + getSyntax());
80 ilm 697
        return this.addColumn(name, typeNames.iterator().next(), getNumberDefault(defaultVal), nullable);
41 ilm 698
    }
699
 
80 ilm 700
    final String getNumberDefault(final Number defaultVal) {
701
        return defaultVal == null ? null : defaultVal.toString();
702
    }
703
 
67 ilm 704
    /**
80 ilm 705
     * Adds a decimal column.
706
     *
707
     * @param name the name of the column.
708
     * @param precision the total number of digits.
709
     * @param scale the number of digits after the decimal point.
710
     * @param defaultVal the default value of the column, can be <code>null</code>, e.g. 3.14.
711
     * @param nullable whether the column accepts NULL.
712
     * @return this.
713
     * @see SQLSyntax#getDecimal(int, int)
714
     * @see SQLSyntax#getDecimalIntPart(int, int)
715
     */
716
    public final T addDecimalColumn(String name, int precision, int scale, BigDecimal defaultVal, boolean nullable) {
717
        return this.addColumn(name, getSyntax().getDecimal(precision, scale), getNumberDefault(defaultVal), nullable);
718
    }
719
 
720
    public final T addBooleanColumn(String name, Boolean defaultVal, boolean nullable) {
142 ilm 721
        final SQLType boolType = SQLType.getBoolean(getSyntax());
722
        return this.addColumn(name, boolType.getTypeName(), boolType.toString(defaultVal), nullable);
80 ilm 723
    }
724
 
725
    /**
67 ilm 726
     * Adds a column.
727
     *
728
     * @param name the name of the column.
729
     * @param sqlType the SQL type, e.g. "double precision" or "varchar(32)".
730
     * @param defaultVal the SQL default value of the column, can be <code>null</code>, e.g. "3.14"
731
     *        or "'small text'".
732
     * @param nullable whether the column accepts NULL.
733
     * @return this.
734
     */
735
    public final T addColumn(String name, String sqlType, String defaultVal, boolean nullable) {
736
        return this.addColumn(name, getSyntax().getFieldDecl(sqlType, defaultVal, nullable));
737
    }
738
 
17 ilm 739
    public abstract T addColumn(String name, String definition);
740
 
741
    public final T addColumn(SQLField f) {
19 ilm 742
        return this.addColumn(f.getName(), f);
17 ilm 743
    }
744
 
19 ilm 745
    public final T addColumn(final String name, SQLField f) {
746
        return this.addColumn(name, this.getSyntax().getFieldDecl(f));
747
    }
748
 
142 ilm 749
    public final boolean addIndex(final Index index) {
750
        // only add index if there won't be an automatic one created. As explained in
751
        // addForeignConstraint() if we did add one in H2 we would need to drop it explicitly.
752
        final boolean add = !this.hasAutomaticIndex(index);
753
        if (add) {
754
            this.addOutsideClause(getSyntax().getCreateIndex(index));
755
        }
756
        return add;
17 ilm 757
    }
758
 
759
    public final T addForeignConstraint(Link l, boolean createIndex) {
67 ilm 760
        return this.addForeignConstraint(FCSpec.createFromLink(l), createIndex);
17 ilm 761
    }
762
 
763
    public final T addForeignConstraint(String fieldName, SQLName refTable, String refCols) {
764
        return this.addForeignConstraint(singletonList(fieldName), refTable, true, singletonList(refCols));
765
    }
766
 
767
    /**
768
     * Adds a foreign constraint specifying that <code>fieldName</code> points to
769
     * <code>refTable</code>.
770
     *
771
     * @param fieldName a field of this table.
772
     * @param refTable the destination of <code>fieldName</code>.
773
     * @param createIndex whether to also create an index on <code>fieldName</code>.
774
     * @param refCols the columns in <code>refTable</code>.
775
     * @return this.
776
     */
777
    public final T addForeignConstraint(final List<String> fieldName, SQLName refTable, boolean createIndex, List<String> refCols) {
57 ilm 778
        return this.addForeignConstraint(new FCSpec(fieldName, refTable, refCols, null, null), createIndex);
41 ilm 779
    }
780
 
142 ilm 781
    // createIndex = false for when we only removeForeignConstraint() without removing the column :
782
    // - if there's was no index, don't add one
783
    // - if there's was one automatic index (e.g. H2) it was dropped and will be added again
784
    // - if there's was one manual index (e.g. pg) it wasn't dropped
41 ilm 785
    public final T addForeignConstraint(final FCSpec fkSpec, boolean createIndex) {
786
        this.fks.add(fkSpec);
142 ilm 787
        // check autoCreatesFKIndex() to avoid creating a duplicate index. Also, on H2, only
788
        // automatically created indexes are automatically dropped ; so if we added one here we
789
        // would need to drop it in AlterTable.dropColumn()
790
        if (createIndex && !getSyntax().getSystem().autoCreatesFKIndex())
144 ilm 791
            this.addOutsideClause(new DeferredClause() {
17 ilm 792
                @Override
793
                public ClauseType getType() {
794
                    return ClauseType.ADD_INDEX;
795
                }
796
 
797
                @Override
144 ilm 798
                public String asString(ChangeTable<?> ct, SQLName tableName) {
799
                    return ct.getSyntax().getCreateIndex("_fki", tableName, fkSpec.getCols());
17 ilm 800
                }
801
            });
802
        return thisAsT();
803
    }
804
 
61 ilm 805
    public final T removeForeignConstraint(final FCSpec fkSpec) {
806
        this.fks.remove(fkSpec);
807
        return thisAsT();
808
    }
809
 
41 ilm 810
    public final List<FCSpec> getForeignConstraints() {
811
        return Collections.unmodifiableList(this.fks);
812
    }
813
 
142 ilm 814
    // true if a foreign constraint will create an equivalent index
815
    private final boolean hasAutomaticIndex(final Index i) {
816
        if (i.isUnique() || !StringUtils.isEmpty(i.getFilter()) || !getSyntax().getSystem().autoCreatesFKIndex())
817
            return false;
818
        for (final FCSpec fc : this.fks) {
819
            if (fc.getCols().equals(i.getCols()))
820
                return true;
821
        }
822
        return false;
823
    }
824
 
17 ilm 825
    // * addForeignColumn = addColumn + addForeignConstraint
826
 
20 ilm 827
    public T addForeignColumn(SQLCreateTableBase<?> createTable) {
80 ilm 828
        return this.addForeignColumn(ForeignColSpec.fromCreateTable(createTable));
20 ilm 829
    }
830
 
17 ilm 831
    /**
20 ilm 832
     * Add a foreign column to a table not yet created.
833
     *
834
     * @param suffix the suffix of the column, used to tell apart multiple columns pointing to the
835
     *        same table, e.g. "" or "2".
836
     * @param createTable the table the new column must point to.
837
     * @return this.
838
     * @see #addForeignColumn(String, SQLCreateTableBase)
839
     */
840
    public T addForeignColumnWithSuffix(String suffix, SQLCreateTableBase<?> createTable) {
80 ilm 841
        return this.addForeignColumn(ForeignColSpec.fromCreateTable(createTable).setColumnNameWithSuffix(suffix));
20 ilm 842
    }
843
 
844
    /**
845
     * Add a foreign column to a table not yet created. Note: this method assumes that the foreign
846
     * table will be created in the same root as this table, like with
847
     * {@link ChangeTable#cat(List, String)}.
848
     *
849
     * @param fk the field name, e.g. "ID_BAT".
850
     * @param createTable the table the new column must point to.
851
     * @return this.
852
     * @see #addForeignColumn(String, SQLName, String, String)
853
     */
854
    public T addForeignColumn(String fk, SQLCreateTableBase<?> createTable) {
80 ilm 855
        return this.addForeignColumn(ForeignColSpec.fromCreateTable(createTable).setColumnName(fk));
20 ilm 856
    }
857
 
858
    /**
17 ilm 859
     * Add a column and its foreign constraint. If <code>table</code> is of length 1 it will be
860
     * prepended the root name of this table.
861
     *
862
     * @param fk the field name, eg "ID_BAT".
863
     * @param table the name of the referenced table, eg BATIMENT.
864
     * @param pk the name of the referenced field, eg "ID".
865
     * @param defaultVal the default value for the column, eg "1".
866
     * @return this.
867
     */
868
    public T addForeignColumn(String fk, SQLName table, String pk, String defaultVal) {
80 ilm 869
        return this.addForeignColumn(new ForeignColSpec(fk, table, pk, defaultVal));
17 ilm 870
    }
871
 
80 ilm 872
    public T addForeignColumn(final ForeignColSpec spec) {
873
        return this.addForeignColumn(spec, null, null);
874
    }
875
 
876
    public T addForeignColumn(final ForeignColSpec spec, final Rule updateRule, final Rule deleteRule) {
877
        this.addColumn(spec.getColumnName(), this.getSyntax().getIDType() + " DEFAULT " + spec.getDefaultVal());
878
        return this.addForeignConstraint(spec.createFCSpec(updateRule, deleteRule), true);
879
    }
880
 
17 ilm 881
    public T addForeignColumn(String fk, SQLTable foreignTable) {
882
        return this.addForeignColumn(fk, foreignTable, true);
883
    }
884
 
885
    /**
886
     * Add a column and its foreign constraint
887
     *
888
     * @param fk the field name, eg "ID_BAT".
889
     * @param foreignTable the referenced table, eg /BATIMENT/.
890
     * @param absolute <code>true</code> if the link should include the whole name of
891
     *        <code>foreignTable</code>, <code>false</code> if the link should just be its name.
892
     * @return this.
893
     * @see #addForeignColumn(String, SQLName, String, String)
894
     */
895
    public T addForeignColumn(String fk, SQLTable foreignTable, final boolean absolute) {
80 ilm 896
        return this.addForeignColumn(ForeignColSpec.fromTable(foreignTable, absolute).setColumnName(fk));
17 ilm 897
    }
898
 
899
    public T addUniqueConstraint(final String name, final List<String> cols) {
83 ilm 900
        return this.addUniqueConstraint(name, cols, null);
901
    }
902
 
903
    /**
904
     * Add a unique constraint. If the table already exists, an initial check will be performed. As
905
     * per the standard <code>NULL</code> means unknown and therefore equal with nothing.
906
     * <p>
907
     * NOTE: on some systems, an index or even triggers will be created instead (particularly with a
908
     * where).
909
     * </p>
910
     *
911
     * @param name name of the constraint.
912
     * @param cols the columns of the constraint, e.g. ["DESIGNATION"].
144 ilm 913
     * @param where an optional where to limit the rows checked, can be <code>null</code>, e.g. "not
914
     *        ARCHIVED".
83 ilm 915
     * @return this.
916
     */
917
    public T addUniqueConstraint(final String name, final List<String> cols, final String where) {
144 ilm 918
        return this.addUniqueConstraint(name, cols, where, null);
919
    }
920
 
921
    /**
922
     * Add a unique constraint with additional data than the other methods. On {@link SQLSystem#H2}
923
     * {@link UniqueConstraintCreatorHelper#getObject(SQLSyntax)} should return a {@link Trigger}
924
     * class to be used instead of requiring to compile Java source code. The class name must end
925
     * with {@value #H2_UNIQUE_TRIGGER_CLASS_SUFFIX} (needed so that {@link SQLTable} can recognize
926
     * it).
927
     *
928
     * @param name name of the constraint.
929
     * @param c data about the constraint.
930
     * @return this.
931
     * @see #addUniqueConstraint(String, List, String)
932
     */
933
    public T addUniqueConstraint(final String name, final UniqueConstraintCreatorHelper c) {
934
        return this.addUniqueConstraint(name, c.getColumns(), c.getWhere(), c);
935
    }
936
 
937
    private T addUniqueConstraint(final String name, final List<String> cols, final String where, final UniqueConstraintCreatorHelper c) {
938
        assert c == null || c.getWhere() == where;
939
        assert c == null || c.getColumns() == cols;
940
        final String comment = c == null ? null : c.getComment();
941
 
83 ilm 942
        final int size = cols.size();
943
        if (size == 0)
944
            throw new IllegalArgumentException("No cols");
945
        final SQLSystem system = getSyntax().getSystem();
946
        // MS treat all NULL equals contrary to the standard
947
        if (system == SQLSystem.MSSQL) {
948
            return this.addOutsideClause(createUniquePartialIndex(name, cols, where));
949
        } else if (where == null) {
144 ilm 950
            return this.addClause(createUniqueConstraint(name, cols));
83 ilm 951
        } else if (system == SQLSystem.POSTGRESQL) {
952
            return this.addOutsideClause(createUniquePartialIndex(name, cols, where));
953
        } else if (system == SQLSystem.H2) {
144 ilm 954
            final String body;
955
            final Class<?> h2TriggerClass = c == null ? null : (Class<?>) c.getObject(getSyntax());
956
            if (h2TriggerClass == null) {
957
                final String javaWhere = StringUtils.doubleQuote(where);
958
                final String javaCols = "java.util.Arrays.asList(" + CollectionUtils.join(cols, ", ", new ITransformer<String, String>() {
83 ilm 959
                    @Override
144 ilm 960
                    public String transformChecked(final String col) {
961
                        return StringUtils.doubleQuote(col);
83 ilm 962
                    }
144 ilm 963
                }) + ")";
964
                body = "AS $$ org.h2.api.Trigger create(){ return new " + PartialUniqueTrigger.class.getName() + "(" + javaCols + ", " + javaWhere + "); } $$";
965
                assert H2_UNIQUE_TRIGGER_PATTERN.matcher(body).find();
966
            } else {
967
                PartialUniqueTrigger h2Trigger;
968
                try {
969
                    h2Trigger = (PartialUniqueTrigger) h2TriggerClass.newInstance();
970
                } catch (Exception e) {
971
                    throw new IllegalArgumentException("Couldn't instantiate " + h2TriggerClass, e);
972
                }
973
                if (!h2Trigger.getColumns().equals(cols) || !Objects.equals(SQLTable.workAroundForH2WhereTrigger(h2Trigger.getWhere()), SQLTable.workAroundForH2WhereTrigger(where)))
974
                    throw new IllegalArgumentException("Wrong parameters returned by " + h2TriggerClass);
975
                if (!h2TriggerClass.getSimpleName().endsWith(H2_UNIQUE_TRIGGER_CLASS_SUFFIX))
976
                    throw new IllegalArgumentException("Class name invalid, must end by \'" + H2_UNIQUE_TRIGGER_CLASS_SUFFIX + "\' : " + h2TriggerClass);
977
                body = "CALL " + SQLBase.quoteIdentifier(h2TriggerClass.getName());
978
                // already checked above
979
                assert H2_UNIQUE_TRIGGER_CLASS_PATTERN.matcher(body).find();
17 ilm 980
            }
144 ilm 981
            final UniqueTrigger trigger = new UniqueTrigger(name, Arrays.asList(TRIGGER_EVENTS), comment) {
83 ilm 982
                @Override
983
                protected String getBody(SQLName tableName) {
984
                    return body;
985
                }
144 ilm 986
 
987
                @Override
988
                protected String getInitialCheckBody(SQLName tableName) {
989
                    final String select = getInitialCheckSelect(cols, where, tableName);
990
                    // TODO should implement SIGNAL instead of abusing CSVREAD
991
                    return "SELECT CASE WHEN (" + select + ") > 0 then CSVREAD('Unique constraint violation') else 'OK' end case;";
992
                }
993
            };
83 ilm 994
            // initial select to check uniqueness
142 ilm 995
            if (checkExistingUniqueness()) {
144 ilm 996
                this.addOutsideClause(trigger.createInitialCheckClause());
83 ilm 997
            }
144 ilm 998
            return this.addOutsideClause(trigger);
999
        } else if (system == SQLSystem.MYSQL) {
1000
            final UniqueTrigger trigger = new UniqueTrigger(name, Arrays.asList(TRIGGER_EVENTS[0]), comment) {
83 ilm 1001
                @Override
1002
                protected String getBody(final SQLName tableName) {
1003
                    final String body = "BEGIN IF " + getNotNullWhere(cols, "NEW.") + " THEN\n" +
1004
                    //
142 ilm 1005
                    "IF ( SELECT COUNT(*) from " + tableName + " where " + where + " and " + CollectionUtils.join(cols, " and ", new ITransformer<String, String>() {
1006
                        @Override
1007
                        public String transformChecked(String col) {
1008
                            return SQLBase.quoteIdentifier(col) + " = NEW." + SQLBase.quoteIdentifier(col);
1009
                        }
1010
                    }) + ") > 1 then\n" + MYSQL_TRIGGER_EXCEPTION + "; END IF; \n"
1011
                    // don't put newline right after semicolon to avoid splitting here
83 ilm 1012
                            + "END IF; \n" + "END";
1013
                    return body;
1014
                }
144 ilm 1015
 
1016
                @Override
1017
                protected String getInitialCheckBody(SQLName tableName) {
1018
                    final String procName = SQLBase.quoteIdentifier("checkUniqueness_" + tableName.getName());
1019
                    String res = "DROP PROCEDURE IF EXISTS " + procName + ";\n";
1020
                    res += "CREATE PROCEDURE " + procName + "() BEGIN\n";
1021
                    final String select = getInitialCheckSelect(cols, where, tableName);
1022
                    // don't put newline right after semicolon to avoid splitting here
1023
                    res += "IF (" + select + ") > 0 THEN " + MYSQL_TRIGGER_EXCEPTION + "; END IF; \n";
1024
                    res += "END;\n";
1025
                    res += "CALL " + procName + ";";
1026
                    return res;
1027
                }
83 ilm 1028
            };
144 ilm 1029
            // initial select to check uniqueness
1030
            if (checkExistingUniqueness()) {
1031
                this.addOutsideClause(trigger.createInitialCheckClause());
1032
            }
1033
 
83 ilm 1034
            this.addOutsideClause(trigger);
1035
            for (int i = 1; i < TRIGGER_EVENTS.length; i++) {
144 ilm 1036
                this.addOutsideClause(new UniqueTrigger(name, Arrays.asList(TRIGGER_EVENTS[i]), comment) {
83 ilm 1037
                    @Override
1038
                    protected String getBody(final SQLName tableName) {
1039
                        return trigger.getBody(tableName);
1040
                    }
144 ilm 1041
 
1042
                    @Override
1043
                    protected String getInitialCheckBody(SQLName tableName) {
1044
                        return trigger.getInitialCheckBody(tableName);
1045
                    }
83 ilm 1046
                });
1047
            }
1048
            return thisAsT();
1049
        } else {
1050
            throw new UnsupportedOperationException("System isn't supported : " + system);
1051
        }
1052
    }
1053
 
144 ilm 1054
    protected final DeferredClause createUniqueConstraint(final String name, final List<String> cols) {
1055
        return new DeferredClause() {
1056
            @Override
1057
            public String asString(ChangeTable<?> ct, SQLName tableName) {
1058
                return ct.getConstraintPrefix() + "CONSTRAINT " + getQuotedConstraintName(tableName, name) + " UNIQUE (" + SQLSyntax.quoteIdentifiers(cols) + ")";
1059
            }
1060
 
1061
            @Override
1062
            public ClauseType getType() {
1063
                return ClauseType.ADD_CONSTRAINT;
1064
            }
1065
        };
1066
    }
1067
 
142 ilm 1068
    // can't be inlined with javac 1.6
1069
    private boolean checkExistingUniqueness() {
1070
        return this instanceof AlterTable;
1071
    }
1072
 
83 ilm 1073
    protected final DeferredClause createUniquePartialIndex(final String name, final List<String> cols, final String userWhere) {
1074
        // http://stackoverflow.com/questions/767657/how-do-i-create-a-unique-constraint-that-also-allows-nulls
1075
        final Where notNullWhere = getSyntax().getSystem() == SQLSystem.MSSQL ? Where.createRaw(getNotNullWhere(cols)) : null;
1076
        final Where w = Where.and(notNullWhere, Where.createRaw(userWhere));
1077
        return getSyntax().getCreateIndex(new SQLIndex(name, cols, true, true, w.toString()));
1078
    }
1079
 
1080
    // Null is equal with nothing :
1081
    // http://www.postgresql.org/docs/9.4/static/ddl-constraints.html#DDL-CONSTRAINTS-UNIQUE-CONSTRAINTS
1082
    static private String getNotNullWhere(final List<String> cols) {
1083
        return getNotNullWhere(cols, "");
1084
    }
1085
 
1086
    static private String getNotNullWhere(final List<String> cols, final String prefix) {
1087
        return CollectionUtils.join(cols, " and ", new ITransformer<String, String>() {
17 ilm 1088
            @Override
83 ilm 1089
            public String transformChecked(String col) {
1090
                return prefix + SQLBase.quoteIdentifier(col) + " IS NOT NULL";
17 ilm 1091
            }
1092
        });
1093
    }
1094
 
83 ilm 1095
    static protected final String getQuotedConstraintName(final SQLName tableName, final String name) {
1096
        return SQLBase.quoteIdentifier(getIndexName(tableName, name));
1097
    }
1098
 
1099
    static protected final String getIndexName(final SQLName tableName, final String name) {
1100
        // for many systems (at least pg & h2) constraint names must be unique in a schema
1101
        return SQLSyntax.getSchemaUniqueName(tableName.getName(), name);
1102
    }
1103
 
1104
    static private SQLName getTriggerName(final SQLName tableName, final String indexName, final String event) {
1105
        // put the trigger in the same schema (tidier and required for MySQL)
1106
        return new SQLName(tableName.getItem(-2), SQLSyntax.getSchemaUniqueName(tableName.getName(), indexName + getTriggerSuffix(event)));
1107
    }
1108
 
144 ilm 1109
    static private abstract class UniqueTrigger extends DeferredClause {
83 ilm 1110
 
1111
        private final String indexName;
1112
        private final List<String> events;
144 ilm 1113
        private final String comment;
83 ilm 1114
 
144 ilm 1115
        public UniqueTrigger(String indexName, final List<String> events, final String comment) {
83 ilm 1116
            super();
1117
            this.indexName = indexName;
1118
            this.events = events;
144 ilm 1119
            this.comment = comment;
83 ilm 1120
        }
1121
 
1122
        @Override
1123
        public final ClauseType getType() {
144 ilm 1124
            return ClauseType.OTHER_ADD;
83 ilm 1125
        }
1126
 
1127
        @Override
1128
        public final String asString(ChangeTable<?> ct, SQLName tableName) {
1129
            // if there's only one event, it means the system doesn't support multiple so we need to
1130
            // get a unique trigger name for each one
1131
            final SQLName triggerName = getTriggerName(tableName, this.indexName, CollectionUtils.getSole(this.events));
144 ilm 1132
            final String createTriggerSQL = "CREATE TRIGGER " + triggerName + " AFTER " + CollectionUtils.join(this.events, ", ") + " on " + tableName + " FOR EACH ROW " + this.getBody(tableName)
1133
                    + ';';
1134
            return this.comment == null ? createTriggerSQL : "-- " + this.comment + "\n" + createTriggerSQL;
83 ilm 1135
        }
1136
 
1137
        protected abstract String getBody(SQLName tableName);
144 ilm 1138
 
1139
        protected abstract String getInitialCheckBody(SQLName tableName);
1140
 
1141
        protected final String getInitialCheckSelect(final List<String> cols, final String where, SQLName tableName) {
1142
            final Where notNullWhere = Where.createRaw(getNotNullWhere(cols));
1143
            final Where w = Where.and(notNullWhere, Where.createRaw(where));
1144
            return "SELECT count(*) FROM " + tableName + " where " + w + " group by " + SQLSyntax.quoteIdentifiers(cols) + " having count(*)>1";
1145
        }
1146
 
1147
        protected final DeferredClause createInitialCheckClause() {
1148
            return new DeferredClause() {
1149
                @Override
1150
                public ClauseType getType() {
1151
                    // same type so that this is executed just before
1152
                    return UniqueTrigger.this.getType();
1153
                }
1154
 
1155
                @Override
1156
                public String asString(ChangeTable<?> ct, SQLName tableName) {
1157
                    return getInitialCheckBody(tableName);
1158
                }
1159
            };
1160
        }
83 ilm 1161
    }
1162
 
144 ilm 1163
    static protected final class DropUniqueTrigger extends DeferredClause {
83 ilm 1164
 
1165
        private final String indexName;
1166
        private final String event;
1167
 
1168
        protected DropUniqueTrigger(String indexName) {
1169
            this(indexName, null);
1170
        }
1171
 
1172
        protected DropUniqueTrigger(String indexName, String event) {
1173
            super();
1174
            this.indexName = indexName;
1175
            this.event = event;
1176
        }
1177
 
1178
        @Override
1179
        public final ClauseType getType() {
144 ilm 1180
            return ClauseType.OTHER_DROP;
83 ilm 1181
        }
1182
 
1183
        @Override
1184
        public final String asString(ChangeTable<?> ct, SQLName tableName) {
1185
            return "DROP TRIGGER IF EXISTS " + getTriggerName(tableName, this.indexName, this.event) + ";";
1186
        }
1187
    }
1188
 
20 ilm 1189
    protected abstract String getConstraintPrefix();
17 ilm 1190
 
1191
    /**
1192
     * Add a clause inside the "CREATE TABLE".
1193
     *
1194
     * @param s the clause to add, eg "CONSTRAINT c UNIQUE field".
1195
     * @param type type of clause.
1196
     * @return this.
1197
     */
1198
    public final T addClause(String s, final ClauseType type) {
144 ilm 1199
        this.clauses.getInClauses().addClause(s, type);
17 ilm 1200
        return thisAsT();
1201
    }
1202
 
144 ilm 1203
    protected final InAndOutClauses getClauses() {
1204
        final InAndOutClauses res = new InAndOutClauses(this.clauses);
1205
        this.modifyClauses(res);
142 ilm 1206
        return res;
1207
    }
1208
 
144 ilm 1209
    protected void modifyClauses(InAndOutClauses res) {
1210
        res.getInClauses().addAllClauses(this.getForeignConstraintsClauses());
17 ilm 1211
    }
1212
 
1213
    public final T addClause(DeferredClause s) {
144 ilm 1214
        this.clauses.getInClauses().addClause(s);
17 ilm 1215
        return thisAsT();
1216
    }
1217
 
1218
    /**
1219
     * Add a clause outside the "CREATE TABLE".
1220
     *
1221
     * @param s the clause to add, <code>null</code> being ignored, e.g. "CREATE INDEX ... ;".
1222
     * @return this.
1223
     */
1224
    public final T addOutsideClause(DeferredClause s) {
144 ilm 1225
        this.clauses.getOutClauses().addClause(s);
17 ilm 1226
        return thisAsT();
1227
    }
1228
 
67 ilm 1229
    public final String asString() {
1230
        return this.asString(NameTransformer.NOP);
1231
    }
1232
 
1233
    public final String asString(final String rootName) {
1234
        return this.asString(new ChangeRootNameTransformer(rootName));
1235
    }
1236
 
17 ilm 1237
    // we can't implement asString() since our subclasses have more parameters
1238
    // so we implement outClausesAsString()
67 ilm 1239
    public abstract String asString(final NameTransformer transf);
17 ilm 1240
 
1241
    // called by #cat()
67 ilm 1242
    protected abstract String asString(final NameTransformer transf, final ConcatStep step);
17 ilm 1243
 
1244
    // [ CONSTRAINT "BATIMENT_ID_SITE_fkey" FOREIGN KEY ("ID_SITE") REFERENCES "SITE"("ID") ON
1245
    // DELETE CASCADE; ]
144 ilm 1246
    protected final List<DeferredGeneralClause> getForeignConstraintsClauses() {
1247
        final List<DeferredGeneralClause> res = new ArrayList<>(this.fks.size());
41 ilm 1248
        for (final FCSpec fk : this.fks) {
144 ilm 1249
            res.add(new DeferredGeneralClause() {
1250
 
1251
                @Override
1252
                public ClauseType getType() {
1253
                    return ClauseType.ADD_CONSTRAINT;
1254
                }
1255
 
1256
                @Override
1257
                public String asString(ChangeTable<?> ct, SQLName tableName, NameTransformer transf) {
1258
                    // resolve relative path, a table is identified by root.table
1259
                    final SQLName relRefTable = fk.getRefTable();
1260
                    final SQLName refTable = transf.transformLinkDestTableName(getRootName(), getName(), relRefTable);
1261
                    return ct.getConstraintPrefix() + ct.getSyntax().getFK(tableName.getName(), fk.getCols(), refTable, fk.getRefCols(), fk.getUpdateRule(), fk.getDeleteRule());
1262
                }
1263
            });
17 ilm 1264
        }
1265
        return res;
1266
    }
1267
 
1268
    @Override
1269
    public String toString() {
67 ilm 1270
        return this.asString();
17 ilm 1271
    }
1272
 
1273
    public final String getName() {
1274
        return this.name;
1275
    }
1276
 
1277
    public final void setName(String name) {
1278
        this.name = name;
1279
    }
1280
 
67 ilm 1281
    public final String getRootName() {
1282
        return this.rootName;
1283
    }
1284
 
144 ilm 1285
    // not public since the only use of the NameTransformer parameter is for foreign constraints.
1286
    // Client code can thus use DeferredClause.
1287
    protected static interface DeferredGeneralClause {
17 ilm 1288
        // ct necessary because CREATE TABLE( CONSTRAINT ) can become ALTER TABLE ADD CONSTRAINT
1289
        // necessary since the full name of the table is only known in #asString(String)
144 ilm 1290
        public String asString(final ChangeTable<?> ct, final SQLName tableName, final NameTransformer transf);
17 ilm 1291
 
1292
        public ClauseType getType();
1293
    }
1294
 
144 ilm 1295
    public static abstract class DeferredClause implements DeferredGeneralClause {
1296
        protected abstract String asString(final ChangeTable<?> ct, final SQLName tableName);
17 ilm 1297
 
144 ilm 1298
        public final String asString(final ChangeTable<?> ct, final SQLName tableName, final NameTransformer transf) {
1299
            return this.asString(ct, tableName);
17 ilm 1300
        }
1301
    }
1302
}