001package io.ebeaninternal.dbmigration.ddlgeneration.platform;
002
003import java.util.ArrayList;
004import java.util.List;
005
006import io.ebean.config.dbplatform.DatabasePlatform;
007import io.ebean.util.StringHelper;
008import io.ebeaninternal.dbmigration.ddlgeneration.DdlAlterTable;
009import io.ebeaninternal.dbmigration.ddlgeneration.DdlBuffer;
010import io.ebeaninternal.dbmigration.ddlgeneration.DdlWrite;
011import io.ebeaninternal.dbmigration.migration.AlterColumn;
012import io.ebeaninternal.dbmigration.migration.Column;
013
014/**
015 * DB2 platform specific DDL.
016 */
017public class DB2Ddl extends PlatformDdl {
018  private static final String MOVE_TABLE = "CALL SYSPROC.ADMIN_MOVE_TABLE(CURRENT_SCHEMA,'%s','%s','%s','%s','','','','','','MOVE')";
019
020  public DB2Ddl(DatabasePlatform platform) {
021    super(platform);
022    this.dropTableIfExists = "drop table ";
023    this.dropSequenceIfExists = "drop sequence ";
024    this.dropConstraintIfExists = "NOT USED";
025    this.dropIndexIfExists = "NOT USED";
026    this.identitySuffix = " generated by default as identity";
027    this.columnSetNull = "drop not null";
028    this.columnSetType = "set data type ";
029    this.inlineUniqueWhenNullable = false;
030    this.historyDdl = new Db2HistoryDdl();
031  }
032
033  @Override
034  public String alterTableTablespace(String tablename, String tableSpace, String indexSpace, String lobSpace) {
035    if(tableSpace == null) {
036      // if no tableSpace set, use the default tablespace USERSPACE1
037      return String.format(MOVE_TABLE, tablename.toUpperCase(), "USERSPACE1", "USERSPACE1", "USERSPACE1");
038    } else {
039      return String.format(MOVE_TABLE, tablename.toUpperCase(), tableSpace, indexSpace, lobSpace);
040    }
041  }
042  
043  @Override
044  public String alterTableAddUniqueConstraint(String tableName, String uqName, String[] columns, String[] nullableColumns) {
045    if (nullableColumns == null || nullableColumns.length == 0) {
046      return super.alterTableAddUniqueConstraint(tableName, uqName, columns, nullableColumns);
047    }     
048
049    if (uqName == null) {
050      throw new NullPointerException();
051    }
052    StringBuilder sb = new StringBuilder("create unique index ");
053    sb.append(maxConstraintName(uqName)).append(" on ").append(tableName).append('(');
054
055    for (int i = 0; i < columns.length; i++) {
056      if (i > 0) {
057        sb.append(",");
058      }
059      sb.append(columns[i]);
060    }
061    sb.append(") exclude null keys");
062    return sb.toString();
063  }
064
065  @Override
066  public void addTablespace(DdlBuffer apply, String tablespaceName, String indexTablespace, String lobTablespace) {
067    apply.append(" in ").append(tablespaceName).append(" index in ").append(indexTablespace).append(" long in ").append(lobTablespace);
068  }
069  
070  @Override
071  public void alterTableAddColumn(DdlWrite writer, String tableName, Column column, boolean onHistoryTable, String defaultValue) {
072
073    String convertedType = convert(column.getType());
074    DdlBuffer buffer = alterTable(writer, tableName).append(addColumn, column.getName());
075    buffer.append(convertedType);
076
077    // Add default value also to history table if it is not excluded
078    if (defaultValue != null) {
079      buffer.append(" default ");
080      buffer.append(defaultValue);
081    }
082
083    if (isTrue(column.isNotnull())) {
084      buffer.appendWithSpace(columnNotNull);
085    }
086    // DB2 History table must match exact!
087    if (!onHistoryTable) {
088      // check constraints cannot be added in one statement for h2
089      if (!StringHelper.isNull(column.getCheckConstraint())) {
090        String ddl = alterTableAddCheckConstraint(tableName, column.getCheckConstraintName(), column.getCheckConstraint());
091        writer.applyPostAlter().appendStatement(ddl);
092      }
093    }
094
095  }
096  @Override
097  public String alterTableDropForeignKey(String tableName, String fkName) {
098    return alterTableDropConstraint(tableName, fkName);
099  };
100
101  @Override
102  public String alterTableDropUniqueConstraint(String tableName, String uniqueConstraintName) {
103    return alterTableDropConstraint(tableName, uniqueConstraintName)
104      + "\n" + dropIndex(uniqueConstraintName, tableName);
105  }
106
107  private void assertNoSchema(String objName) {
108    if (objName.indexOf('.') != -1) {
109      throw new UnsupportedOperationException("Schemas are not yet supported. ObjectName: '" + objName + "'");
110    }
111  }
112  @Override
113  public String alterTableDropConstraint(String tableName, String constraintName) {
114    assertNoSchema(tableName);
115    StringBuilder sb = new StringBuilder(300);
116    sb.append("delimiter $$\n")
117      .append("begin\n")
118      .append("if exists (select constname from syscat.tabconst where tabschema = current_schema and ucase(constname) = '")
119      .append(maxConstraintName(constraintName).toUpperCase())
120
121      .append("' and ucase(tabname) = '").append(naming.normaliseTable(tableName).toUpperCase()).append("') then\n")
122
123      .append("  prepare stmt from 'alter table ").append(tableName)
124      .append(" drop constraint ").append(maxConstraintName(constraintName)).append("';\n")
125
126      .append("  execute stmt;\n")
127      .append("end if;\n")
128      .append("end$$");
129    return sb.toString();
130
131  }
132
133  @Override
134  public String dropIndex(String indexName, String tableName, boolean concurrent) {
135    assertNoSchema(indexName);
136    StringBuilder sb = new StringBuilder(300);
137    sb.append("delimiter $$\n")
138      .append("begin\n")
139      .append("if exists (select indname from syscat.indexes where indschema = current_schema and ucase(indname) = '")
140      .append(maxConstraintName(indexName).toUpperCase()).append("') then\n")
141      .append("  prepare stmt from 'drop index ").append(maxConstraintName(indexName)).append("';\n")
142      .append("  execute stmt;\n")
143      .append("end if;\n")
144      .append("end$$");
145    return sb.toString();
146  }
147
148  @Override
149  public String dropSequence(String sequenceName) {
150    assertNoSchema(sequenceName);
151    StringBuilder sb = new StringBuilder(300);
152    sb.append("delimiter $$\n");
153    sb.append("begin\n");
154    sb.append("if exists (select seqschema from syscat.sequences where seqschema = current_schema and ucase(seqname) = '")
155      .append(maxConstraintName(sequenceName).toUpperCase()).append("') then\n");
156    sb.append("  prepare stmt from 'drop sequence ").append(maxConstraintName(sequenceName)).append("';\n");
157    sb.append("  execute stmt;\n");
158    sb.append("end if;\n");
159    sb.append("end$$");
160    return sb.toString();
161  }
162
163  @Override
164  protected void alterColumnType(DdlWrite writer, AlterColumn alter) {
165    String type = convert(alter.getType());
166    DB2ColumnOptionsParser parser = new DB2ColumnOptionsParser(type);
167    alterTable(writer, alter.getTableName()).append(alterColumn, alter.getColumnName())
168      .append(columnSetType).append(parser.getType());
169
170    if (parser.getInlineLength() != null) {
171      alterTable(writer, alter.getTableName()).append(alterColumn, alter.getColumnName())
172        .append("set").appendWithSpace(parser.getInlineLength());
173    }
174
175    if (parser.hasExtraOptions()) {
176      alterTable(writer, alter.getTableName()).raw("-- ignored options for ")
177        .append(alter.getTableName()).append(".").append(alter.getColumnName())
178        .append(": compact=").append(String.valueOf(parser.isCompact()))
179        .append(", logged=").append(String.valueOf(parser.isLogged()));
180    }
181  }
182
183  @Override
184  protected DdlAlterTable alterTable(DdlWrite writer, String tableName) {
185    return writer.applyAlterTable(tableName, Db2AlterTableWrite::new);
186  };
187
188  class Db2AlterTableWrite extends BaseAlterTableWrite {
189
190    public Db2AlterTableWrite(String tableName) {
191      super(tableName, DB2Ddl.this);
192    }
193
194    @Override
195    protected List<AlterCmd> postProcessCommands(List<AlterCmd> cmds) {
196      List<AlterCmd> ret = new ArrayList<>(cmds.size() + 1);
197      boolean requiresReorg = false;
198      for (AlterCmd cmd : cmds) {
199        ret.add(cmd);
200        if (!requiresReorg && checkReorg(cmd)) {
201          requiresReorg = true;
202        }
203      }
204      if (requiresReorg) {
205        ret.add(newRawCommand("call sysproc.admin_cmd('reorg table " + tableName() + "')"));
206      }
207      return ret;
208    }
209
210    /**
211     * determine, if we need a reorg.
212     * 
213     * See: https://www.ibm.com/docs/en/db2/11.5?topic=statements-alter-table The following is the full list of REORG-recommended
214     * ALTER statements that cause a version change and place the table into a REORG-pending state:
215     * <ul>
216     * <li>DROP COLUMN
217     * <li>ALTER COLUMN SET NOT NULL
218     * <li>ALTER COLUMN DROP NOT NULL
219     * <li>ALTER COLUMN SET DATA TYPE, except in the following situations:<br>
220     * Increasing the length of a VARCHAR or VARGRAPHIC column<br>
221     * Decreasing the length of a VARCHAR or VARGRAPHIC column without truncating trailing blanks from existing data, when no indexes
222     * exist on the column
223     * </ul>
224     * 
225     */
226    private boolean checkReorg(AlterCmd cmd) {
227      switch (cmd.getOperation()) {
228      case "drop column":
229        return true;
230      case "alter column":
231        String alter = cmd.getAlternation();
232        return alter.equals("set not null")
233          || alter.equals("drop not default")
234          || alter.startsWith("set data type"); // note: altering varchar length only is not detected here
235      default:
236        return false;
237      }
238    }
239  }
240}