001package io.ebeaninternal.dbmigration.ddlgeneration.platform;
002
003import io.ebean.config.DatabaseConfig;
004import io.ebean.config.dbplatform.DatabasePlatform;
005import io.ebean.config.dbplatform.DbPlatformType;
006import io.ebeaninternal.dbmigration.ddlgeneration.DdlBuffer;
007import io.ebeaninternal.dbmigration.ddlgeneration.DdlHandler;
008import io.ebeaninternal.dbmigration.migration.AlterColumn;
009
010import java.io.IOException;
011import java.util.Objects;
012import java.util.regex.Matcher;
013import java.util.regex.Pattern;
014
015public abstract class AbstractHanaDdl extends PlatformDdl {
016
017  private static final Pattern ARRAY_PATTERN = Pattern.compile("(\\w+)\\s*\\[\\s*\\]\\s*(\\(\\d+\\))?", Pattern.CASE_INSENSITIVE);
018
019  public AbstractHanaDdl(DatabasePlatform platform) {
020    super(platform);
021    this.addColumn = "add (";
022    this.addColumnSuffix = ")";
023    this.alterColumn = "alter (";
024    this.alterColumnSuffix = ")";
025    this.columnDropDefault = " default null";
026    this.columnSetDefault = " default";
027    this.columnSetNotnull = " not null";
028    this.columnSetNull = " null";
029    this.dropColumn = "drop (";
030    this.dropColumnSuffix = ")";
031    this.dropConstraintIfExists = "drop constraint ";
032    this.dropIndexIfExists = "drop index ";
033    this.dropSequenceIfExists = "drop sequence ";
034    this.dropTableCascade = " cascade";
035    this.dropTableIfExists = "drop table ";
036    this.fallbackArrayType = "nvarchar(1000)";
037    this.historyDdl = new HanaHistoryDdl();
038    this.identitySuffix = " generated by default as identity";
039  }
040
041  @Override
042  public String alterColumnBaseAttributes(AlterColumn alter) {
043    String tableName = alter.getTableName();
044    String columnName = alter.getColumnName();
045    String currentType = alter.getCurrentType();
046    String type = alter.getType() != null ? alter.getType() : currentType;
047    type = convert(type);
048    currentType = convert(currentType);
049    boolean notnull = (alter.isNotnull() != null) ? alter.isNotnull() : Boolean.TRUE.equals(alter.isCurrentNotnull());
050    String notnullClause = notnull ? " not null" : "";
051    String defaultValue = DdlHelp.isDropDefault(alter.getDefaultValue()) ? "null"
052      : (alter.getDefaultValue() != null ? alter.getDefaultValue() : alter.getCurrentDefaultValue());
053    String defaultValueClause = (defaultValue == null || defaultValue.isEmpty()) ? "" : " default " + defaultValue;
054
055    try {
056      DdlBuffer buffer = new BaseDdlBuffer(null);
057      if (!isConvertible(currentType, type)) {
058        // add an intermediate conversion if possible
059        if (isNumberType(currentType)) {
060          // numbers can always be converted to decimal
061          buffer.append("alter table ").append(tableName).append(" ").append(alterColumn).append(" ").append(columnName)
062            .append(" decimal ").append(defaultValueClause).append(notnullClause).append(alterColumnSuffix)
063            .endOfStatement();
064
065        } else if (isStringType(currentType)) {
066          // strings can always be converted to nclob
067          buffer.append("alter table ").append(tableName).append(" ").append(alterColumn).append(" ").append(columnName)
068            .append(" nclob ").append(defaultValueClause).append(notnullClause).append(alterColumnSuffix)
069            .endOfStatement();
070        }
071      }
072
073      buffer.append("alter table ").append(tableName).append(" ").append(alterColumn).append(" ").append(columnName)
074        .append(" ").append(type).append(defaultValueClause).append(notnullClause).append(alterColumnSuffix);
075
076      return buffer.getBuffer();
077    } catch (IOException e) {
078      throw new RuntimeException(e);
079    }
080  }
081
082  @Override
083  public String alterColumnDefaultValue(String tableName, String columnName, String defaultValue) {
084    throw new UnsupportedOperationException();
085  }
086
087  @Override
088  public String alterColumnNotnull(String tableName, String columnName, boolean notnull) {
089    return null;
090  }
091
092  @Override
093  public DdlHandler createDdlHandler(DatabaseConfig config) {
094    return new HanaDdlHandler(config, this);
095  }
096
097  @Override
098  public String alterColumnType(String tableName, String columnName, String type) {
099    return null;
100  }
101
102  @Override
103  protected String convertArrayType(String logicalArrayType) {
104    Matcher matcher = ARRAY_PATTERN.matcher(logicalArrayType);
105    if (matcher.matches()) {
106      return convert(matcher.group(1)) + " array" + (matcher.group(2) == null ? "" : matcher.group(2));
107    } else {
108      return fallbackArrayType;
109    }
110  }
111
112  @Override
113  public String alterTableAddUniqueConstraint(String tableName, String uqName, String[] columns, String[] nullableColumns) {
114    if (nullableColumns == null || nullableColumns.length == 0) {
115      return super.alterTableAddUniqueConstraint(tableName, uqName, columns, nullableColumns);
116    } else {
117      return "-- cannot create unique index \"" + uqName + "\" on table \"" + tableName + "\" with nullable columns";
118    }
119  }
120
121  @Override
122  public String alterTableDropUniqueConstraint(String tableName, String uniqueConstraintName) {
123    DdlBuffer buffer = new BaseDdlBuffer(null);
124    try {
125      buffer.append("delimiter $$").newLine();
126      buffer.append("do").newLine();
127      buffer.append("begin").newLine();
128      buffer.append("declare exit handler for sql_error_code 397 begin end").endOfStatement();
129      buffer.append("exec 'alter table ").append(tableName).append(" ").append(dropUniqueConstraint).append(" ")
130        .append(maxConstraintName(uniqueConstraintName)).append("'").endOfStatement();
131      buffer.append("end").endOfStatement();
132      buffer.append("$$");
133      return buffer.getBuffer();
134    } catch (IOException e) {
135      throw new RuntimeException(e);
136    }
137  }
138
139  @Override
140  public String alterTableDropConstraint(String tableName, String constraintName) {
141    return alterTableDropUniqueConstraint(tableName, constraintName);
142  }
143
144  /**
145   * It is rather complex to delete a column on HANA as there must not exist any
146   * foreign keys. That's why we call a user stored procedure here
147   */
148  @Override
149  public void alterTableDropColumn(DdlBuffer buffer, String tableName, String columnName) throws IOException {
150    buffer.append("CALL usp_ebean_drop_column('").append(tableName).append("', '").append(columnName).append("')")
151      .endOfStatement();
152  }
153
154  /**
155   * Check if a data type can be converted to another data type. Data types can't
156   * be converted if the target type has a lower precision than the source type.
157   *
158   * @param sourceType The source data type
159   * @param targetType the target data type
160   * @return {@code true} if the type can be converted, {@code false} otherwise
161   */
162  private boolean isConvertible(String sourceType, String targetType) {
163    if (Objects.equals(sourceType, targetType)) {
164      return true;
165    }
166
167    if (sourceType == null || targetType == null) {
168      return true;
169    }
170
171    if ("bigint".equals(sourceType)) {
172      if ("integer".equals(targetType) || "smallint".equals(targetType) || "tinyint".equals(targetType)) {
173        return false;
174      }
175    } else if ("integer".equals(sourceType)) {
176      if ("smallint".equals(targetType) || "tinyint".equals(targetType)) {
177        return false;
178      }
179    } else if ("smallint".equals(sourceType)) {
180      if ("tinyint".equals(targetType)) {
181        return false;
182      }
183    } else if ("double".equals(sourceType)) {
184      if ("real".equals(targetType)) {
185        return false;
186      }
187    }
188
189    DbPlatformType dbPlatformSourceType = DbPlatformType.parse(sourceType);
190
191    if ("float".equals(dbPlatformSourceType.getName())) {
192      if ("real".equals(targetType)) {
193        return false;
194      }
195    } else if ("varchar".equals(dbPlatformSourceType.getName()) || "nvarchar".equals(dbPlatformSourceType.getName())) {
196      DbPlatformType dbPlatformTargetType = DbPlatformType.parse(targetType);
197      if ("varchar".equals(dbPlatformTargetType.getName()) || "nvarchar".equals(dbPlatformTargetType.getName())) {
198        if (dbPlatformSourceType.getDefaultLength() > dbPlatformTargetType.getDefaultLength()) {
199          return false;
200        }
201      }
202    } else if ("decimal".equals(dbPlatformSourceType.getName())) {
203      DbPlatformType dbPlatformTargetType = DbPlatformType.parse(targetType);
204      if ("decimal".equals(dbPlatformTargetType.getName())) {
205        if (dbPlatformSourceType.getDefaultLength() > dbPlatformTargetType.getDefaultLength()
206          || dbPlatformSourceType.getDefaultScale() > dbPlatformTargetType.getDefaultScale()) {
207          return false;
208        }
209      }
210    }
211
212    return true;
213  }
214
215  private boolean isNumberType(String type) {
216    return type != null
217      && ("bigint".equals(type) || "integer".equals(type) || "smallint".equals(type) || "tinyint".equals(type)
218      || type.startsWith("float") || "real".equals(type) || "double".equals(type) || type.startsWith("decimal"));
219  }
220
221  private boolean isStringType(String type) {
222    return type != null
223      && (type.startsWith("varchar") || type.startsWith("nvarchar") || "clob".equals(type) || "nclob".equals(type));
224  }
225}