001package io.ebeaninternal.dbmigration.ddlgeneration.platform;
002
003import io.ebean.annotation.Platform;
004import io.ebean.config.DatabaseConfig;
005import io.ebean.config.DbConstraintNaming;
006import io.ebean.config.NamingConvention;
007import io.ebean.config.dbplatform.DbHistorySupport;
008import io.ebean.config.dbplatform.IdType;
009import io.ebean.util.StringHelper;
010import io.ebeaninternal.dbmigration.ddlgeneration.DdlBuffer;
011import io.ebeaninternal.dbmigration.ddlgeneration.DdlOptions;
012import io.ebeaninternal.dbmigration.ddlgeneration.DdlWrite;
013import io.ebeaninternal.dbmigration.ddlgeneration.TableDdl;
014import io.ebeaninternal.dbmigration.ddlgeneration.platform.util.IndexSet;
015import io.ebeaninternal.dbmigration.migration.AddColumn;
016import io.ebeaninternal.dbmigration.migration.AddHistoryTable;
017import io.ebeaninternal.dbmigration.migration.AddTableComment;
018import io.ebeaninternal.dbmigration.migration.AddUniqueConstraint;
019import io.ebeaninternal.dbmigration.migration.AlterColumn;
020import io.ebeaninternal.dbmigration.migration.AlterForeignKey;
021import io.ebeaninternal.dbmigration.migration.Column;
022import io.ebeaninternal.dbmigration.migration.CreateIndex;
023import io.ebeaninternal.dbmigration.migration.CreateTable;
024import io.ebeaninternal.dbmigration.migration.DdlScript;
025import io.ebeaninternal.dbmigration.migration.DropColumn;
026import io.ebeaninternal.dbmigration.migration.DropHistoryTable;
027import io.ebeaninternal.dbmigration.migration.DropIndex;
028import io.ebeaninternal.dbmigration.migration.DropTable;
029import io.ebeaninternal.dbmigration.migration.ForeignKey;
030import io.ebeaninternal.dbmigration.migration.UniqueConstraint;
031import io.ebeaninternal.dbmigration.model.MTable;
032import io.ebeaninternal.dbmigration.model.MTableIdentity;
033import io.ebeaninternal.server.deploy.IdentityMode;
034
035import java.io.IOException;
036import java.util.ArrayList;
037import java.util.Collections;
038import java.util.LinkedHashMap;
039import java.util.List;
040import java.util.Map;
041
042import static io.ebean.util.StringHelper.replace;
043import static io.ebeaninternal.api.PlatformMatch.matchPlatform;
044import static io.ebeaninternal.dbmigration.ddlgeneration.platform.SplitColumns.split;
045
046/**
047 * Base implementation for 'create table' and 'alter table' statements.
048 */
049public class BaseTableDdl implements TableDdl {
050
051  enum HistorySupport {
052    NONE,
053    SQL2011,
054    TRIGGER_BASED
055  }
056
057  protected final DbConstraintNaming naming;
058
059  protected final NamingConvention namingConvention;
060
061  protected final PlatformDdl platformDdl;
062
063  protected final String historyTableSuffix;
064
065  /**
066   * Used to check that indexes on foreign keys should be skipped as a unique index on the columns
067   * already exists.
068   */
069  protected final IndexSet indexSet = new IndexSet();
070
071  /**
072   * Used when unique constraints specifically for OneToOne can't be created normally (MsSqlServer).
073   */
074  protected final List<Column> externalUnique = new ArrayList<>();
075
076  protected final List<UniqueConstraint> externalCompoundUnique = new ArrayList<>();
077
078  // counters used when constraint names are truncated due to maximum length
079  // and these counters are used to keep the constraint name unique
080  protected int countCheck;
081  protected int countUnique;
082  protected int countForeignKey;
083  protected int countIndex;
084
085  /**
086   * Base tables that have associated history tables that need their triggers/functions regenerated as
087   * columns have been added, removed, included or excluded.
088   */
089  protected final Map<String, HistoryTableUpdate> regenerateHistoryTriggers = new LinkedHashMap<>();
090
091  private final boolean strictMode;
092
093  private final HistorySupport historySupport;
094
095  /**
096   * Helper class that is used to execute the migration ddl before and after the migration action.
097   */
098  private class DdlMigrationHelp {
099    private final List<String> before;
100    private final List<String> after;
101    private final String tableName;
102    private final String columnName;
103    private final String defaultValue;
104    private final boolean withHistory;
105
106    /**
107     * Constructor for DdlMigrationHelp when adding a NEW column.
108     */
109    DdlMigrationHelp(String tableName, Column column, boolean withHistory) {
110      this.tableName = tableName;
111      this.columnName = column.getName();
112      this.defaultValue = platformDdl.convertDefaultValue(column.getDefaultValue());
113      boolean alterNotNull = Boolean.TRUE.equals(column.isNotnull());
114      if (column.getBefore().isEmpty() && alterNotNull && defaultValue == null) {
115        handleStrictError(tableName, columnName);
116      }
117      before = getScriptsForPlatform(column.getBefore());
118      after = getScriptsForPlatform(column.getAfter());
119      this.withHistory = withHistory;
120    }
121
122    /**
123     * Constructor for DdlMigrationHelp when altering a column.
124     */
125    DdlMigrationHelp(AlterColumn alter) {
126      this.tableName = alter.getTableName();
127      this.columnName = alter.getColumnName();
128      String tmp = alter.getDefaultValue() != null ? alter.getDefaultValue() : alter.getCurrentDefaultValue();
129      this.defaultValue = platformDdl.convertDefaultValue(tmp);
130      boolean alterNotNull = Boolean.TRUE.equals(alter.isNotnull());
131      // here we add the platform's default update script
132      withHistory = isTrue(alter.isWithHistory());
133      if (alter.getBefore().isEmpty() && alterNotNull) {
134        if (defaultValue == null) {
135          handleStrictError(tableName, columnName);
136        }
137        before = Collections.singletonList(platformDdl.getUpdateNullWithDefault());
138      } else {
139        before = getScriptsForPlatform(alter.getBefore());
140      }
141      after = getScriptsForPlatform(alter.getAfter());
142    }
143
144    void writeBefore(DdlBuffer buffer) throws IOException {
145      if (!before.isEmpty()) {
146        buffer.end();
147      }
148
149      if (!before.isEmpty() && withHistory) {
150        buffer.append("-- NOTE: table has @History - special migration may be necessary").newLine();
151      }
152      for (String ddlScript : before) {
153        buffer.appendStatement(translate(ddlScript, tableName, columnName, this.defaultValue));
154      }
155    }
156
157    void writeAfter(DdlBuffer buffer) throws IOException {
158      if (!after.isEmpty() && withHistory) {
159        buffer.append("-- NOTE: table has @History - special migration may be necessary").newLine();
160      }
161      // here we run post migration scripts
162      for (String ddlScript : after) {
163        buffer.appendStatement(translate(ddlScript, tableName, columnName, defaultValue));
164      }
165      if (!after.isEmpty()) {
166        buffer.end();
167      }
168    }
169
170    private List<String> getScriptsForPlatform(List<DdlScript> scripts) {
171      Platform searchPlatform = platformDdl.getPlatform().getPlatform();
172      for (DdlScript script : scripts) {
173        if (matchPlatform(searchPlatform, script.getPlatforms())) {
174          // just returns the first match (rather than appends them)
175          return script.getDdl();
176        }
177      }
178      return Collections.emptyList();
179    }
180
181    /**
182     * Replaces Table name (${table}), Column name (${column}) and default value (${default}) in DDL.
183     */
184    private String translate(String ddl, String tableName, String columnName, String defaultValue) {
185      String ret = replace(ddl, "${table}", tableName);
186      ret = replace(ret, "${column}", columnName);
187      return replace(ret, "${default}", defaultValue);
188    }
189
190    private void handleStrictError(String tableName, String columnName) {
191      if (strictMode) {
192        String message = "DB Migration of non-null column with no default value specified for: " + tableName + "." + columnName+" Use @DbDefault to specify a default value or specify dbMigration.setStrictMode(false)";
193        throw new IllegalArgumentException(message);
194      }
195    }
196
197    public String getDefaultValue() {
198      return defaultValue;
199    }
200  }
201
202  /**
203   * Construct with a naming convention and platform specific DDL.
204   */
205  public BaseTableDdl(DatabaseConfig config, PlatformDdl platformDdl) {
206    this.namingConvention = config.getNamingConvention();
207    this.naming = config.getConstraintNaming();
208    this.historyTableSuffix = config.getHistoryTableSuffix();
209    this.platformDdl = platformDdl;
210    this.platformDdl.configure(config);
211    this.strictMode = config.isDdlStrictMode();
212    DbHistorySupport hist = platformDdl.getPlatform().getHistorySupport();
213    if (hist == null) {
214      this.historySupport = HistorySupport.NONE;
215    } else {
216      this.historySupport = hist.isStandardsBased() ? HistorySupport.SQL2011 : HistorySupport.TRIGGER_BASED;
217    }
218  }
219
220  /**
221   * Reset counters and index set for each table processed.
222   */
223  protected void reset() {
224    indexSet.clear();
225    externalUnique.clear();
226    externalCompoundUnique.clear();
227    countCheck = 0;
228    countUnique = 0;
229    countForeignKey = 0;
230    countIndex = 0;
231  }
232
233  /**
234   * Generate the appropriate 'create table' and matching 'drop table' statements
235   * and add them to the appropriate 'apply' and 'rollback' buffers.
236   */
237  @Override
238  public void generate(DdlWrite writer, CreateTable createTable) throws IOException {
239    reset();
240
241    String tableName = lowerTableName(createTable.getName());
242    List<Column> columns = createTable.getColumn();
243    List<Column> pk = determinePrimaryKeyColumns(columns);
244
245    DdlIdentity identity = DdlIdentity.NONE;
246    if ((pk.size() == 1)) {
247      final IdentityMode identityMode = MTableIdentity.fromCreateTable(createTable);
248      IdType idType = platformDdl.useIdentityType(identityMode.getIdType());
249      String sequenceName = identityMode.getSequenceName();
250      if (IdType.SEQUENCE == idType && (sequenceName == null || sequenceName.isEmpty())) {
251        sequenceName = sequenceName(createTable, pk);
252      }
253      identity = new DdlIdentity(idType, identityMode, sequenceName);
254    }
255
256    String partitionMode = createTable.getPartitionMode();
257
258    DdlBuffer apply = writer.apply();
259    apply.append(platformDdl.getCreateTableCommandPrefix()).append(" ").append(tableName).append(" (");
260    writeTableColumns(apply, columns, identity);
261    writeUniqueConstraints(apply, createTable);
262    writeCompoundUniqueConstraints(apply, createTable);
263    if (!pk.isEmpty()) {
264      // defined on the columns
265      if (partitionMode == null || !platformDdl.suppressPrimaryKeyOnPartition()) {
266        writePrimaryKeyConstraint(apply, createTable.getPkName(), toColumnNames(pk));
267      }
268    }
269    if (platformDdl.isInlineForeignKeys()) {
270      writeInlineForeignKeys(writer, createTable);
271    }
272    apply.newLine().append(")");
273    addTableStorageEngine(apply, createTable);
274    addTableCommentInline(apply, createTable);
275    if (partitionMode != null) {
276      platformDdl.addTablePartition(apply, partitionMode, createTable.getPartitionColumn());
277    }
278    apply.endOfStatement();
279
280    addComments(apply, createTable);
281    writeUniqueOneToOneConstraints(writer, createTable);
282    if (isTrue(createTable.isWithHistory())) {
283      // create history with rollback before the
284      // associated drop table is written to rollback
285      createWithHistory(writer, createTable.getName());
286    }
287
288    // add drop table to the rollback buffer - do this before
289    // we drop the related sequence (if sequences are used)
290    dropTable(writer.dropAll(), tableName);
291
292    if (identity.useSequence()) {
293      writeSequence(writer, identity);
294    }
295
296    // add blank line for a bit of whitespace between tables
297    apply.end();
298    writer.dropAll().end();
299    if (!platformDdl.isInlineForeignKeys()) {
300      writeAddForeignKeys(writer, createTable);
301    }
302  }
303
304  private String sequenceName(CreateTable createTable, List<Column> pk) {
305    return namingConvention.getSequenceName(createTable.getName(), pk.get(0).getName());
306  }
307
308  /**
309   * Add table and column comments (separate from the create table statement).
310   */
311  private void addComments(DdlBuffer apply, CreateTable createTable) throws IOException {
312    if (!platformDdl.isInlineComments()) {
313      String tableComment = createTable.getComment();
314      if (hasValue(tableComment)) {
315        platformDdl.addTableComment(apply, createTable.getName(), tableComment);
316      }
317      for (Column column : createTable.getColumn()) {
318        if (!StringHelper.isNull(column.getComment())) {
319          platformDdl.addColumnComment(apply, createTable.getName(), column.getName(), column.getComment());
320        }
321      }
322    }
323  }
324
325  /**
326   * Add the table storage engine clause.
327   */
328  private void addTableStorageEngine(DdlBuffer apply, CreateTable createTable) throws IOException {
329    if (platformDdl.isIncludeStorageEngine()) {
330      platformDdl.tableStorageEngine(apply, createTable.getStorageEngine());
331    }
332  }
333
334  /**
335   * Add the table comment inline with the create table statement.
336   */
337  private void addTableCommentInline(DdlBuffer apply, CreateTable createTable) throws IOException {
338    if (platformDdl.isInlineComments()) {
339      String tableComment = createTable.getComment();
340      if (!StringHelper.isNull(tableComment)) {
341        platformDdl.inlineTableComment(apply, tableComment);
342      }
343    }
344  }
345
346  private void writeTableColumns(DdlBuffer apply, List<Column> columns, DdlIdentity identity) throws IOException {
347    platformDdl.writeTableColumns(apply, columns, identity);
348  }
349
350  /**
351   * Specific handling of OneToOne unique constraints for MsSqlServer.
352   * For all other DB platforms these unique constraints are done inline as per normal.
353   */
354  protected void writeUniqueOneToOneConstraints(DdlWrite write, CreateTable createTable) throws IOException {
355    String tableName = createTable.getName();
356    for (Column col : externalUnique) {
357      String uqName = col.getUniqueOneToOne();
358      if (uqName == null) {
359        uqName = col.getUnique();
360      }
361      String[] columnNames = {col.getName()};
362      write.apply().appendStatement(platformDdl.alterTableAddUniqueConstraint(tableName, uqName, columnNames, Boolean.TRUE.equals(col.isNotnull()) ? null : columnNames));
363      write.dropAllForeignKeys().appendStatement(platformDdl.dropIndex(uqName, tableName));
364    }
365
366    for (UniqueConstraint constraint : externalCompoundUnique) {
367      String uqName = constraint.getName();
368      String[] columnNames = split(constraint.getColumnNames());
369      String[] nullableColumns = split(constraint.getNullableColumns());
370
371      write.apply().appendStatement(platformDdl.alterTableAddUniqueConstraint(tableName, uqName, columnNames, nullableColumns));
372      write.dropAllForeignKeys().appendStatement(platformDdl.dropIndex(uqName, tableName));
373    }
374  }
375
376  protected void writeSequence(DdlWrite writer, DdlIdentity identity) throws IOException {
377    String seqName = identity.getSequenceName();
378    String createSeq = platformDdl.createSequence(seqName, identity);
379    if (hasValue(createSeq)) {
380      writer.apply().append(createSeq).newLine();
381      writer.dropAll().appendStatement(platformDdl.dropSequence(seqName));
382    }
383  }
384
385  protected void createWithHistory(DdlWrite writer, String name) throws IOException {
386    MTable table = writer.getTable(name);
387    platformDdl.createWithHistory(writer, table);
388  }
389
390  protected void writeInlineForeignKeys(DdlWrite write, CreateTable createTable) throws IOException {
391    for (Column column : createTable.getColumn()) {
392      String references = column.getReferences();
393      if (hasValue(references)) {
394        writeInlineForeignKey(write, column);
395      }
396    }
397    writeInlineCompoundForeignKeys(write, createTable);
398  }
399
400  protected void writeInlineForeignKey(DdlWrite write, Column column) throws IOException {
401    String fkConstraint = platformDdl.tableInlineForeignKey(new WriteForeignKey(null, column));
402    write.apply().append(",").newLine().append("  ").append(fkConstraint);
403  }
404
405  protected void writeInlineCompoundForeignKeys(DdlWrite write, CreateTable createTable) throws IOException {
406    for (ForeignKey key : createTable.getForeignKey()) {
407      String fkConstraint = platformDdl.tableInlineForeignKey(new WriteForeignKey(null, key));
408      write.apply().append(",").newLine().append("  ").append(fkConstraint);
409    }
410  }
411
412  protected void writeAddForeignKeys(DdlWrite write, CreateTable createTable) throws IOException {
413    for (Column column : createTable.getColumn()) {
414      String references = column.getReferences();
415      if (hasValue(references)) {
416        writeForeignKey(write, createTable.getName(), column);
417      }
418    }
419    writeAddCompoundForeignKeys(write, createTable);
420  }
421
422  protected void writeAddCompoundForeignKeys(DdlWrite write, CreateTable createTable) throws IOException {
423    for (ForeignKey key : createTable.getForeignKey()) {
424      writeForeignKey(write, new WriteForeignKey(createTable.getName(), key));
425    }
426  }
427
428  protected void writeForeignKey(DdlWrite write, String tableName, Column column) throws IOException {
429    writeForeignKey(write, new WriteForeignKey(tableName, column));
430  }
431
432  protected void writeForeignKey(DdlWrite write, WriteForeignKey request) throws IOException {
433    DdlBuffer fkeyBuffer = write.applyForeignKeys();
434    String tableName = lowerTableName(request.table());
435    if (request.indexName() != null) {
436      // no matching unique constraint so add the index
437      fkeyBuffer.appendStatement(platformDdl.createIndex(new WriteCreateIndex(request.indexName(), tableName, request.cols(), false)));
438    }
439    alterTableAddForeignKey(write.getOptions(), fkeyBuffer, request);
440    fkeyBuffer.end();
441
442    write.dropAllForeignKeys().appendStatement(platformDdl.alterTableDropForeignKey(tableName, request.fkName()));
443    if (hasValue(request.indexName())) {
444      write.dropAllForeignKeys().appendStatement(platformDdl.dropIndex(request.indexName(), tableName));
445    }
446    write.dropAllForeignKeys().end();
447  }
448
449  protected void alterTableAddForeignKey(DdlOptions options, DdlBuffer buffer, WriteForeignKey request) throws IOException {
450    buffer.appendStatement(platformDdl.alterTableAddForeignKey(options, request));
451  }
452
453  protected void appendColumns(String[] columns, DdlBuffer buffer) throws IOException {
454    buffer.append(" (");
455    for (int i = 0; i < columns.length; i++) {
456      if (i > 0) {
457        buffer.append(",");
458      }
459      buffer.append(lowerColumnName(columns[i].trim()));
460    }
461    buffer.append(")");
462  }
463
464  /**
465   * Add 'drop table' statement to the buffer.
466   */
467  protected void dropTable(DdlBuffer buffer, String tableName) throws IOException {
468    buffer.appendStatement(platformDdl.dropTable(tableName));
469  }
470
471  /**
472   * Add 'drop sequence' statement to the buffer.
473   */
474  protected void dropSequence(DdlBuffer buffer, String sequenceName) throws IOException {
475    buffer.appendStatement(platformDdl.dropSequence(sequenceName));
476  }
477
478  protected void writeCompoundUniqueConstraints(DdlBuffer apply, CreateTable createTable) throws IOException {
479    boolean inlineUniqueWhenNull = platformDdl.isInlineUniqueWhenNullable();
480    for (UniqueConstraint uniqueConstraint : createTable.getUniqueConstraint()) {
481      if (platformInclude(uniqueConstraint.getPlatforms())) {
482        if (inlineUniqueWhenNull) {
483          String uqName = uniqueConstraint.getName();
484          apply.append(",").newLine();
485          apply.append("  constraint ").append(uqName).append(" unique");
486          appendColumns(split(uniqueConstraint.getColumnNames()), apply);
487        } else {
488          externalCompoundUnique.add(uniqueConstraint);
489        }
490      }
491    }
492  }
493
494  private boolean platformInclude(String platforms) {
495    return matchPlatform(platformDdl.getPlatform().getPlatform(), platforms);
496  }
497
498  /**
499   * Write the unique constraints inline with the create table statement.
500   */
501  protected void writeUniqueConstraints(DdlBuffer apply, CreateTable createTable) throws IOException {
502    boolean inlineUniqueWhenNullable = platformDdl.isInlineUniqueWhenNullable();
503    List<Column> columns = new WriteUniqueConstraint(createTable.getColumn()).uniqueKeys();
504    for (Column column : columns) {
505      if (Boolean.TRUE.equals(column.isNotnull()) || inlineUniqueWhenNullable) {
506        // normal mechanism for adding unique constraint
507        inlineUniqueConstraintSingle(apply, column);
508      } else {
509        // SqlServer & DB2 specific mechanism for adding unique constraints (that allow nulls)
510        externalUnique.add(column);
511      }
512    }
513  }
514
515  /**
516   * Write the unique constraint inline with the create table statement.
517   */
518  protected void inlineUniqueConstraintSingle(DdlBuffer buffer, Column column) throws IOException {
519    String uqName = column.getUnique();
520    if (uqName == null) {
521      uqName = column.getUniqueOneToOne();
522    }
523    buffer.append(",").newLine();
524    buffer.append("  constraint ").append(uqName).append(" unique ");
525    buffer.append("(");
526    buffer.append(lowerColumnName(column.getName()));
527    buffer.append(")");
528  }
529
530  /**
531   * Write the primary key constraint inline with the create table statement.
532   */
533  protected void writePrimaryKeyConstraint(DdlBuffer buffer, String pkName, String[] pkColumns) throws IOException {
534    buffer.append(",").newLine();
535    buffer.append("  constraint ").append(pkName).append(" primary key");
536    appendColumns(pkColumns, buffer);
537  }
538
539  /**
540   * Return as an array of string column names.
541   */
542  protected String[] toColumnNames(List<Column> columns) {
543    String[] cols = new String[columns.size()];
544    for (int i = 0; i < cols.length; i++) {
545      cols[i] = columns.get(i).getName();
546    }
547    return cols;
548  }
549
550  /**
551   * Convert the table lower case.
552   */
553  protected String lowerTableName(String name) {
554    return naming.lowerTableName(name);
555  }
556
557  /**
558   * Convert the column name to lower case.
559   */
560  protected String lowerColumnName(String name) {
561    return naming.lowerColumnName(name);
562  }
563
564  /**
565   * Return the list of columns that make the primary key.
566   */
567  protected List<Column> determinePrimaryKeyColumns(List<Column> columns) {
568    List<Column> pk = new ArrayList<>(3);
569    for (Column column : columns) {
570      if (isTrue(column.isPrimaryKey())) {
571        pk.add(column);
572      }
573    }
574    return pk;
575  }
576
577  @Override
578  public void generate(DdlWrite writer, CreateIndex index) throws IOException {
579    if (platformInclude(index.getPlatforms())) {
580      writer.apply().appendStatement(platformDdl.createIndex(new WriteCreateIndex(index)));
581      writer.dropAll().appendStatement(platformDdl.dropIndex(index.getIndexName(), index.getTableName(), Boolean.TRUE.equals(index.isConcurrent())));
582    }
583  }
584
585  @Override
586  public void generate(DdlWrite writer, DropIndex dropIndex) throws IOException {
587    if (platformInclude(dropIndex.getPlatforms())) {
588      writer.apply().appendStatement(platformDdl.dropIndex(dropIndex.getIndexName(), dropIndex.getTableName(), Boolean.TRUE.equals(dropIndex.isConcurrent())));
589    }
590  }
591
592  @Override
593  public void generate(DdlWrite writer, AddUniqueConstraint constraint) throws IOException {
594    if (platformInclude(constraint.getPlatforms())) {
595      if (DdlHelp.isDropConstraint(constraint.getColumnNames())) {
596        writer.apply().appendStatement(platformDdl.alterTableDropUniqueConstraint(constraint.getTableName(), constraint.getConstraintName()));
597
598      } else {
599        String[] cols = split(constraint.getColumnNames());
600        String[] nullableColumns = split(constraint.getNullableColumns());
601        writer.apply().appendStatement(platformDdl.alterTableAddUniqueConstraint(constraint.getTableName(), constraint.getConstraintName(), cols, nullableColumns));
602      }
603    }
604  }
605
606  @Override
607  public void generate(DdlWrite writer, AlterForeignKey alterForeignKey) throws IOException {
608    if (DdlHelp.isDropForeignKey(alterForeignKey.getColumnNames())) {
609      writer.apply().appendStatement(platformDdl.alterTableDropForeignKey(alterForeignKey.getTableName(), alterForeignKey.getName()));
610    } else {
611      writer.apply().appendStatement(platformDdl.alterTableAddForeignKey(writer.getOptions(), new WriteForeignKey(alterForeignKey)));
612    }
613  }
614
615  /**
616   * Add add history table DDL.
617   */
618  @Override
619  public void generate(DdlWrite writer, AddHistoryTable addHistoryTable) throws IOException {
620    platformDdl.addHistoryTable(writer, addHistoryTable);
621  }
622
623  /**
624   * Add drop history table DDL.
625   */
626  @Override
627  public void generate(DdlWrite writer, DropHistoryTable dropHistoryTable) throws IOException {
628    platformDdl.dropHistoryTable(writer, dropHistoryTable);
629  }
630
631  @Override
632  public void generateProlog(DdlWrite write) throws IOException {
633    platformDdl.generateProlog(write);
634  }
635
636  /**
637   * Called at the end to generate additional ddl such as regenerate history triggers.
638   */
639  @Override
640  public void generateEpilog(DdlWrite write) throws IOException {
641    if (!regenerateHistoryTriggers.isEmpty()) {
642      platformDdl.lockTables(write.applyHistoryTrigger(), regenerateHistoryTriggers.keySet());
643
644      for (HistoryTableUpdate update : this.regenerateHistoryTriggers.values()) {
645        platformDdl.regenerateHistoryTriggers(write, update);
646      }
647
648      platformDdl.unlockTables(write.applyHistoryTrigger(), regenerateHistoryTriggers.keySet());
649    }
650    platformDdl.generateEpilog(write);
651  }
652
653  @Override
654  public void generate(DdlWrite writer, AddTableComment addTableComment) throws IOException {
655    if (hasValue(addTableComment.getComment())) {
656      platformDdl.addTableComment(writer.apply(), addTableComment.getName(), addTableComment.getComment());
657    }
658  }
659
660  /**
661   * Add add column DDL.
662   */
663  @Override
664  public void generate(DdlWrite writer, AddColumn addColumn) throws IOException {
665    String tableName = addColumn.getTableName();
666    List<Column> columns = addColumn.getColumn();
667    for (Column column : columns) {
668      alterTableAddColumn(writer.apply(), tableName, column, false, isTrue(addColumn.isWithHistory()));
669    }
670    if (isTrue(addColumn.isWithHistory()) && historySupport == HistorySupport.TRIGGER_BASED) {
671      // make same changes to the history table
672      String historyTable = historyTable(tableName);
673      for (Column column : columns) {
674        regenerateHistoryTriggers(tableName, HistoryTableUpdate.Change.ADD, column.getName());
675        alterTableAddColumn(writer.apply(), historyTable, column, true, true);
676      }
677    }
678    for (Column column : columns) {
679      if (hasValue(column.getReferences())) {
680        writeForeignKey(writer, tableName, column);
681      }
682    }
683    writer.apply().end();
684  }
685
686  /**
687   * Add drop table DDL.
688   */
689  @Override
690  public void generate(DdlWrite writer, DropTable dropTable) throws IOException {
691    dropTable(writer.apply(), dropTable.getName());
692    if (hasValue(dropTable.getSequenceCol())
693        && platformDdl.getPlatform().getDbIdentity().isSupportsSequence()) {
694      String sequenceName = dropTable.getSequenceName();
695      if (!hasValue(sequenceName)) {
696        sequenceName = namingConvention.getSequenceName(dropTable.getName(), dropTable.getSequenceCol());
697      }
698      dropSequence(writer.apply(), sequenceName);
699    }
700  }
701
702  /**
703   * Add drop column DDL.
704   */
705  @Override
706  public void generate(DdlWrite writer, DropColumn dropColumn) throws IOException {
707    String tableName = dropColumn.getTableName();
708    alterTableDropColumn(writer.apply(), tableName, dropColumn.getColumnName());
709
710    if (isTrue(dropColumn.isWithHistory())  && historySupport == HistorySupport.TRIGGER_BASED) {
711      // also drop from the history table
712      regenerateHistoryTriggers(tableName, HistoryTableUpdate.Change.DROP, dropColumn.getColumnName());
713      alterTableDropColumn(writer.apply(), historyTable(tableName), dropColumn.getColumnName());
714    }
715    writer.apply().end();
716  }
717
718  /**
719   * Add all the appropriate changes based on the column changes.
720   */
721  @Override
722  public void generate(DdlWrite writer, AlterColumn alterColumn) throws IOException {
723    DdlMigrationHelp ddlHelp = new DdlMigrationHelp(alterColumn);
724    ddlHelp.writeBefore(writer.apply());
725
726    if (isTrue(alterColumn.isHistoryExclude())) {
727      regenerateHistoryTriggers(alterColumn.getTableName(), HistoryTableUpdate.Change.EXCLUDE, alterColumn.getColumnName());
728    } else if (isFalse(alterColumn.isHistoryExclude())) {
729      regenerateHistoryTriggers(alterColumn.getTableName(), HistoryTableUpdate.Change.INCLUDE, alterColumn.getColumnName());
730    }
731
732    if (hasValue(alterColumn.getDropForeignKey())) {
733      alterColumnDropForeignKey(writer, alterColumn);
734    }
735    if (hasValue(alterColumn.getReferences())) {
736      alterColumnAddForeignKey(writer, alterColumn);
737    }
738
739    if (hasValue(alterColumn.getDropUnique())) {
740      alterColumnDropUniqueConstraint(writer, alterColumn);
741    }
742    if (hasValue(alterColumn.getUnique())) {
743      alterColumnAddUniqueConstraint(writer, alterColumn);
744    }
745    if (hasValue(alterColumn.getUniqueOneToOne())) {
746      alterColumnAddUniqueOneToOneConstraint(writer, alterColumn);
747    }
748    if (hasValue(alterColumn.getComment())) {
749      alterColumnComment(writer, alterColumn);
750    }
751    if (hasValue(alterColumn.getDropCheckConstraint())) {
752      dropCheckConstraint(writer, alterColumn, alterColumn.getDropCheckConstraint());
753    }
754
755    boolean alterCheckConstraint = hasValue(alterColumn.getCheckConstraint());
756
757    if (alterCheckConstraint) {
758      // drop constraint before altering type etc
759      dropCheckConstraint(writer, alterColumn, alterColumn.getCheckConstraintName());
760    }
761    boolean alterBaseAttributes = false;
762    if (hasValue(alterColumn.getType())) {
763      alterColumnType(writer, alterColumn);
764      alterBaseAttributes = true;
765    }
766    if (hasValue(alterColumn.getDefaultValue())) {
767      alterColumnDefaultValue(writer, alterColumn);
768      alterBaseAttributes = true;
769    }
770    if (alterColumn.isNotnull() != null) {
771      alterColumnNotnull(writer, alterColumn);
772      alterBaseAttributes = true;
773    }
774    if (alterBaseAttributes) {
775      alterColumnBaseAttributes(writer, alterColumn);
776    }
777    if (alterCheckConstraint) {
778      // add constraint last (after potential type change)
779      addCheckConstraint(writer, alterColumn);
780    }
781    ddlHelp.writeAfter(writer.apply());
782  }
783
784  private void alterColumnComment(DdlWrite writer, AlterColumn alterColumn) throws IOException {
785    platformDdl.addColumnComment(writer.apply(), alterColumn.getTableName(), alterColumn.getColumnName(), alterColumn.getComment());
786  }
787
788  /**
789   * Return the name of the history table given the base table name.
790   */
791  protected String historyTable(String baseTable) {
792    return baseTable + historyTableSuffix;
793  }
794
795  /**
796   * Register the base table that we need to regenerate the history triggers on.
797   */
798  protected void regenerateHistoryTriggers(String baseTableName, HistoryTableUpdate.Change change, String column) {
799    HistoryTableUpdate update = regenerateHistoryTriggers.computeIfAbsent(baseTableName, HistoryTableUpdate::new);
800    update.add(change, column);
801  }
802
803  /**
804   * This is mysql specific - alter all the base attributes of the column together.
805   * Will be called, if there is a type, dbdefault or notnull change.
806   */
807  protected void alterColumnBaseAttributes(DdlWrite writer, AlterColumn alter) throws IOException {
808    String ddl = platformDdl.alterColumnBaseAttributes(alter);
809    if (hasValue(ddl)) {
810      writer.apply().appendStatement(ddl);
811
812      if (isTrue(alter.isWithHistory()) && alter.getType() != null && historySupport == HistorySupport.TRIGGER_BASED) {
813        // mysql and sql server column type change allowing nulls in the history table column
814        regenerateHistoryTriggers(alter.getTableName(), HistoryTableUpdate.Change.ALTER, alter.getColumnName());
815        AlterColumn alterHistoryColumn = new AlterColumn();
816        alterHistoryColumn.setTableName(historyTable(alter.getTableName()));
817        alterHistoryColumn.setColumnName(alter.getColumnName());
818        alterHistoryColumn.setType(alter.getType());
819        String histColumnDdl = platformDdl.alterColumnBaseAttributes(alterHistoryColumn);
820
821        // write the apply to history table
822        writer.apply().appendStatement(histColumnDdl);
823      }
824    }
825  }
826
827  protected void alterColumnDefaultValue(DdlWrite writer, AlterColumn alter) throws IOException {
828    writer.apply().appendStatement(platformDdl.alterColumnDefaultValue(alter.getTableName(), alter.getColumnName(), alter.getDefaultValue()));
829  }
830
831  protected void dropCheckConstraint(DdlWrite writer, AlterColumn alter, String constraintName) throws IOException {
832    writer.apply().appendStatement(platformDdl.alterTableDropConstraint(alter.getTableName(), constraintName));
833  }
834
835  protected void addCheckConstraint(DdlWrite writer, AlterColumn alter) throws IOException {
836    writer.apply().appendStatement(platformDdl.alterTableAddCheckConstraint(alter.getTableName(), alter.getCheckConstraintName(), alter.getCheckConstraint()));
837  }
838
839  protected void alterColumnNotnull(DdlWrite writer, AlterColumn alter) throws IOException {
840    writer.apply().appendStatement(platformDdl.alterColumnNotnull(alter.getTableName(), alter.getColumnName(), alter.isNotnull()));
841  }
842
843  protected void alterColumnType(DdlWrite writer, AlterColumn alter) throws IOException {
844    String ddl = platformDdl.alterColumnType(alter.getTableName(), alter.getColumnName(), alter.getType());
845    if (hasValue(ddl)) {
846      writer.apply().appendStatement(ddl);
847      if (isTrue(alter.isWithHistory()) && historySupport == HistorySupport.TRIGGER_BASED) {
848        regenerateHistoryTriggers(alter.getTableName(), HistoryTableUpdate.Change.ALTER, alter.getColumnName());
849        // apply same type change to matching column in the history table
850        ddl = platformDdl.alterColumnType(historyTable(alter.getTableName()), alter.getColumnName(), alter.getType());
851        writer.apply().appendStatement(ddl);
852      }
853    }
854  }
855
856  protected void alterColumnAddForeignKey(DdlWrite writer, AlterColumn alterColumn) throws IOException {
857    alterTableAddForeignKey(writer.getOptions(), writer.apply(), new WriteForeignKey(alterColumn));
858  }
859
860  protected void alterColumnDropForeignKey(DdlWrite writer, AlterColumn alter) throws IOException {
861    writer.apply().appendStatement(platformDdl.alterTableDropForeignKey(alter.getTableName(), alter.getDropForeignKey()));
862  }
863
864  protected void alterColumnDropUniqueConstraint(DdlWrite writer, AlterColumn alter) throws IOException {
865    writer.apply().appendStatement(platformDdl.alterTableDropUniqueConstraint(alter.getTableName(), alter.getDropUnique()));
866  }
867
868  protected void alterColumnAddUniqueOneToOneConstraint(DdlWrite writer, AlterColumn alter) throws IOException {
869    addUniqueConstraint(writer, alter, alter.getUniqueOneToOne());
870  }
871
872  protected void alterColumnAddUniqueConstraint(DdlWrite writer, AlterColumn alter) throws IOException {
873    addUniqueConstraint(writer, alter, alter.getUnique());
874  }
875
876  protected void addUniqueConstraint(DdlWrite writer, AlterColumn alter, String uqName) throws IOException {
877    String[] cols = {alter.getColumnName()};
878    boolean notNull = alter.isNotnull() != null ? alter.isNotnull() : Boolean.TRUE.equals(alter.isNotnull());
879    writer.apply().appendStatement(platformDdl.alterTableAddUniqueConstraint(alter.getTableName(), uqName, cols, notNull ? null : cols));
880
881    writer.dropAllForeignKeys().appendStatement(platformDdl.dropIndex(uqName, alter.getTableName()));
882  }
883
884
885  protected void alterTableDropColumn(DdlBuffer buffer, String tableName, String columnName) throws IOException {
886    platformDdl.alterTableDropColumn(buffer, tableName, columnName);
887  }
888
889  protected void alterTableAddColumn(DdlBuffer buffer, String tableName, Column column, boolean onHistoryTable, boolean withHistory) throws IOException {
890    DdlMigrationHelp help = new DdlMigrationHelp(tableName, column, withHistory);
891    if (!onHistoryTable) {
892      help.writeBefore(buffer);
893    }
894
895    platformDdl.alterTableAddColumn(buffer, tableName, column, onHistoryTable, help.getDefaultValue());
896    final String comment = column.getComment();
897    if (comment != null && !comment.isEmpty()) {
898      platformDdl.addColumnComment(buffer, tableName, column.getName(), comment);
899    }
900
901    if (!onHistoryTable) {
902      help.writeAfter(buffer);
903    }
904  }
905
906  protected boolean isFalse(Boolean value) {
907    return value != null && !value;
908  }
909
910  /**
911   * Return true if null or trimmed string is empty.
912   */
913  protected boolean hasValue(String value) {
914    return value != null && !value.trim().isEmpty();
915  }
916
917  /**
918   * Null safe Boolean true test.
919   */
920  protected boolean isTrue(Boolean value) {
921    return Boolean.TRUE.equals(value);
922  }
923
924}