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