001package io.ebeaninternal.dbmigration.model.build;
002
003import io.ebean.annotation.Platform;
004import io.ebeaninternal.dbmigration.ddlgeneration.platform.util.IndexSet;
005import io.ebeaninternal.dbmigration.model.*;
006import io.ebeaninternal.server.deploy.*;
007import io.ebeaninternal.server.deploy.visitor.BaseTablePropertyVisitor;
008
009import java.util.*;
010
011/**
012 * Used as part of ModelBuildBeanVisitor and generally adds the MColumn to the associated
013 * MTable model objects.
014 */
015public class ModelBuildPropertyVisitor extends BaseTablePropertyVisitor {
016
017  protected final ModelBuildContext ctx;
018
019  private final MTable table;
020
021  private final BeanDescriptor<?> beanDescriptor;
022
023  private final IndexSet indexSet = new IndexSet();
024
025  private MColumn lastColumn;
026
027  private int countForeignKey;
028  private int countIndex;
029  private int countUnique;
030  private int countCheck;
031
032  public ModelBuildPropertyVisitor(ModelBuildContext ctx, MTable table, BeanDescriptor<?> beanDescriptor) {
033    this.ctx = ctx;
034    this.table = table;
035    this.beanDescriptor = beanDescriptor;
036    addIndexes(beanDescriptor.indexDefinitions());
037  }
038
039  /**
040   * Add unique constraints defined via JPA UniqueConstraint annotations.
041   */
042  private void addIndexes(IndexDefinition[] indexes) {
043    if (indexes != null) {
044      for (IndexDefinition index : indexes) {
045        String[] columns = index.getColumns();
046        indexSet.add(columns);
047        if (index.isUniqueConstraint()) {
048          table.addUniqueConstraint(createMUniqueConstraint(index, columns));
049        } else {
050          // 'just' an index (not a unique constraint)
051          ctx.addIndex(createMIndex(indexName(index), table.getName(), index));
052        }
053      }
054    }
055  }
056
057  private MCompoundUniqueConstraint createMUniqueConstraint(IndexDefinition index, String[] columns) {
058    return new MCompoundUniqueConstraint(columns, false, uniqueConstraintName(index), platforms(index.getPlatforms()));
059  }
060
061  private String uniqueConstraintName(IndexDefinition index) {
062    String uqName = index.getName();
063    if (uqName == null || uqName.trim().isEmpty()) {
064      return uniqueConstraintName(index.getColumns());
065    }
066    return uqName;
067  }
068
069  private String indexName(IndexDefinition index) {
070    String idxName = index.getName();
071    if (idxName == null || idxName.trim().isEmpty()) {
072      idxName = indexName(index.getColumns());
073    }
074    return idxName;
075  }
076
077  private MIndex createMIndex(String indexName, String tableName, IndexDefinition index) {
078    return new MIndex(indexName, tableName, index.getColumns(), platforms(index.getPlatforms()), index.isUnique(), index.isConcurrent(), index.getDefinition());
079  }
080
081  private String platforms(Platform[] platforms) {
082    if (platforms == null || platforms.length == 0) {
083      return null;
084    }
085    StringJoiner joiner = new StringJoiner(",");
086    for (Platform platform : platforms) {
087      joiner.add(platform.name());
088    }
089    return joiner.toString();
090  }
091
092  @Override
093  public void visitEnd() {
094
095    // set the primary key name
096    table.setPkName(primaryKeyName());
097
098    // check if indexes on foreign keys should be suppressed
099    for (MColumn column : table.allColumns()) {
100      if (hasValue(column.getForeignKeyIndex())) {
101        if (indexSet.contains(column.getName())) {
102          // suppress index on foreign key as there is already
103          // effectively an index (probably via unique constraint)
104          column.setForeignKeyIndex(null);
105        }
106      }
107    }
108
109    for (MCompoundForeignKey compoundKey : table.getCompoundKeys()) {
110      if (indexSet.contains(compoundKey.getColumns())) {
111        // suppress index on foreign key as there is already
112        // effectively an index (probably via unique constraint)
113        compoundKey.setIndexName(null);
114      }
115    }
116
117    addDraftTable();
118    table.updateCompoundIndices();
119  }
120
121  /**
122   * Create a 'draft' table that is mostly the same as the base table.
123   * It has @DraftOnly columns and adjusted primary and foreign keys.
124   */
125  private void addDraftTable() {
126    if (beanDescriptor.isDraftable() || beanDescriptor.isDraftableElement()) {
127      // create a 'Draft' table which looks very similar (change PK, FK etc)
128      ctx.createDraft(table, !beanDescriptor.isDraftableElement());
129    }
130  }
131
132
133  @Override
134  public void visitMany(BeanPropertyAssocMany<?> p) {
135    if (p.createJoinTable()) {
136      // only create on other 'owning' side
137
138      // build the create table and fkey constraints
139      // putting the DDL into ctx for later output as we are
140      // in the middle of rendering the create table DDL
141      MTable intersectionTable = new ModelBuildIntersectionTable(ctx, p).build();
142      if (p.isO2mJoinTable()) {
143        intersectionTable.clearForeignKeyIndexes();
144        Collection<MColumn> cols = intersectionTable.allColumns();
145        if (cols.size() == 2) {
146          // always the second column that we put the unique constraint on
147          MColumn col = new ArrayList<>(cols).get(1);
148          col.setUnique(uniqueConstraintName(col.getName()));
149        }
150      }
151    } else if (p.isElementCollection()) {
152      ModelBuildElementTable.build(ctx, p);
153    }
154  }
155
156  @Override
157  public void visitEmbeddedScalar(BeanProperty p, BeanPropertyAssocOne<?> embedded) {
158    if (p instanceof BeanPropertyAssocOne) {
159      visitOneImported((BeanPropertyAssocOne<?>)p);
160    } else {
161      // only allow Nonnull if embedded is Nonnull
162      visitScalar(p, !embedded.isNullable());
163    }
164    if (embedded.isId()) {
165      // compound primary key
166      lastColumn.setPrimaryKey(true);
167    }
168  }
169
170  @Override
171  public void visitOneImported(BeanPropertyAssocOne<?> p) {
172
173    TableJoinColumn[] columns = p.tableJoin().columns();
174    if (columns.length == 0) {
175      throw new RuntimeException("No join columns for " + p.fullName());
176    }
177
178    List<MColumn> modelColumns = new ArrayList<>(columns.length);
179
180    MCompoundForeignKey compoundKey = null;
181    if (columns.length > 1) {
182      // compound foreign key
183      String refTable = p.targetDescriptor().baseTable();
184      String fkName = foreignKeyConstraintName(p.name());
185      String fkIndex = foreignKeyIndexName(p.name());
186      compoundKey = new MCompoundForeignKey(fkName, refTable, fkIndex);
187      table.addForeignKey(compoundKey);
188    }
189
190    for (TableJoinColumn column : columns) {
191
192      String dbCol = column.getLocalDbColumn();
193      BeanProperty importedProperty = p.findMatchImport(dbCol);
194      if (importedProperty == null) {
195        throw new RuntimeException("Imported BeanProperty not found?");
196      }
197      String columnDefn = ctx.getColumnDefn(importedProperty, true);
198      String refColumn = importedProperty.dbColumn();
199
200      MColumn col = table.addColumn(dbCol, columnDefn, !p.isNullable());
201      col.setDbMigrationInfos(p.dbMigrationInfos());
202      col.setDefaultValue(p.dbColumnDefault());
203      if (columns.length == 1) {
204        if (p.hasForeignKeyConstraint() && !importedProperty.descriptor().suppressForeignKey()) {
205          // single references column (put it on the column)
206          String refTable = importedProperty.descriptor().baseTable();
207          if (refTable == null) {
208            // odd case where an EmbeddedId only has 1 property
209            refTable = p.targetDescriptor().baseTable();
210          }
211          col.setReferences(refTable + "." + refColumn);
212          col.setForeignKeyName(foreignKeyConstraintName(col.getName()));
213          if (p.hasForeignKeyIndex()) {
214            col.setForeignKeyIndex(foreignKeyIndexName(col.getName()));
215          }
216          PropertyForeignKey foreignKey = p.foreignKey();
217          if (foreignKey != null) {
218            col.setForeignKeyModes(foreignKey.getOnDelete(), foreignKey.getOnUpdate());
219          }
220        }
221      } else {
222        compoundKey.addColumnPair(dbCol, refColumn);
223      }
224      modelColumns.add(col);
225    }
226
227    if (p.isOneToOne()) {
228      // adding the unique constraint restricts the cardinality from OneToMany down to OneToOne
229      // for MsSqlServer we need different DDL to handle NULL values on this constraint
230      if (modelColumns.size() == 1) {
231        MColumn col = modelColumns.get(0);
232        indexSetAdd(col.getName());
233        col.setUniqueOneToOne(uniqueConstraintName(col.getName()));
234
235      } else {
236        String[] cols = indexSetAdd(toColumnNames(modelColumns));
237        String uqName = uniqueConstraintName(p.name());
238        table.addUniqueConstraint(new MCompoundUniqueConstraint(cols, uqName));
239      }
240    }
241  }
242
243  @Override
244  public void visitScalar(BeanProperty p, boolean allowNonNull) {
245    if (p.isSecondaryTable()) {
246      lastColumn = null;
247      return;
248    }
249
250    // using non-strict mode to render the DB type such that we have a
251    // "logical" type like jsonb(200) that can map to JSONB or VARCHAR(200)
252    MColumn col = new MColumn(p.dbColumn(), ctx.getColumnDefn(p, false));
253    col.setComment(p.dbComment());
254    col.setDraftOnly(p.isDraftOnly());
255    col.setHistoryExclude(p.isExcludedFromHistory());
256
257    if (p.isId() || p.isImportedPrimaryKey()) {
258      col.setPrimaryKey(true);
259      if (p.descriptor().isUseIdGenerator()) {
260        col.setIdentity(true);
261      }
262      TableJoin primaryKeyJoin = p.descriptor().primaryKeyJoin();
263      if (primaryKeyJoin != null && !table.isPartitioned()) {
264        final PropertyForeignKey foreignKey = primaryKeyJoin.getForeignKey();
265        if (foreignKey == null || !foreignKey.isNoConstraint()) {
266          TableJoinColumn[] columns = primaryKeyJoin.columns();
267          col.setReferences(primaryKeyJoin.getTable() + "." + columns[0].getForeignDbColumn());
268          col.setForeignKeyName(foreignKeyConstraintName(col.getName()));
269          if (foreignKey != null) {
270            col.setForeignKeyModes(foreignKey.getOnDelete(), foreignKey.getOnUpdate());
271          }
272        }
273      }
274    } else {
275      col.setDefaultValue(p.dbColumnDefault());
276      if (allowNonNull && (!p.isNullable() || p.isDDLNotNull())) {
277        col.setNotnull(true);
278      }
279    }
280
281    col.setDbMigrationInfos(p.dbMigrationInfos());
282
283    if (p.isUnique() && !p.isId()) {
284      col.setUnique(uniqueConstraintName(col.getName()));
285      indexSetAdd(col.getName());
286    }
287    Set<String> checkConstraintValues = p.dbCheckConstraintValues();
288    if (checkConstraintValues != null) {
289      if (beanDescriptor.hasInheritance()) {
290        InheritInfo inheritInfo = beanDescriptor.inheritInfo();
291        inheritInfo.appendCheckConstraintValues(p.name(), checkConstraintValues);
292      }
293      col.setCheckConstraint(buildCheckConstraint(p.dbColumn(), checkConstraintValues));
294      col.setCheckConstraintName(checkConstraintName(col.getName()));
295    }
296
297    lastColumn = col;
298    table.addColumn(col);
299  }
300
301  /**
302   * Build the check constraint clause given the db column and values.
303   */
304  private String buildCheckConstraint(String dbColumn, Set<String> checkConstraintValues) {
305    StringBuilder sb = new StringBuilder();
306    sb.append("check ( ").append(dbColumn).append(" in (");
307    int count = 0;
308    for (String value : checkConstraintValues) {
309      if (count++ > 0) {
310        sb.append(",");
311      }
312      sb.append(value);
313    }
314    sb.append("))");
315    return sb.toString();
316  }
317
318  private void indexSetAdd(String column) {
319    indexSet.add(column);
320  }
321
322  private String[] indexSetAdd(String[] cols) {
323    indexSet.add(cols);
324    return cols;
325  }
326
327  private String[] toColumnNames(List<MColumn> modelColumns) {
328    String[] cols = new String[modelColumns.size()];
329    for (int i = 0; i < modelColumns.size(); i++) {
330      cols[i] = modelColumns.get(i).getName();
331    }
332    return cols;
333  }
334
335  /**
336   * Return the primary key constraint name.
337   */
338  protected String primaryKeyName() {
339    return ctx.primaryKeyName(table.getName());
340  }
341
342  /**
343   * Return the foreign key constraint name given a single column foreign key.
344   */
345  protected String foreignKeyConstraintName(String columnName) {
346    return ctx.foreignKeyConstraintName(table.getName(), columnName, ++countForeignKey);
347  }
348
349  protected String foreignKeyIndexName(String column) {
350    String[] cols = {column};
351    return foreignKeyIndexName(cols);
352  }
353
354  /**
355   * Return the foreign key constraint name given a single column foreign key.
356   */
357  protected String foreignKeyIndexName(String[] columns) {
358    return ctx.foreignKeyIndexName(table.getName(), columns, ++countIndex);
359  }
360
361  /**
362   * Return the index name given multiple columns.
363   */
364  protected String indexName(String[] columns) {
365    return ctx.indexName(table.getName(), columns, ++countIndex);
366  }
367
368  /**
369   * Return the unique constraint name.
370   */
371  protected String uniqueConstraintName(String columnName) {
372    return ctx.uniqueConstraintName(table.getName(), columnName, ++countUnique);
373  }
374
375  /**
376   * Return the unique constraint name.
377   */
378  protected String uniqueConstraintName(String[] columnNames) {
379    return ctx.uniqueConstraintName(table.getName(), columnNames, ++countUnique);
380  }
381
382  /**
383   * Return the constraint name.
384   */
385  protected String checkConstraintName(String columnName) {
386    return ctx.checkConstraintName(table.getName(), columnName, ++countCheck);
387  }
388
389  private boolean hasValue(String val) {
390    return val != null && !val.isEmpty();
391  }
392
393}