All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.molgenis.data.postgresql.PostgreSqlRepositoryCollection Maven / Gradle / Ivy

There is a newer version: 8.4.5
Show newest version
package org.molgenis.data.postgresql;

import static com.google.common.collect.Sets.immutableEnumSet;
import static java.lang.String.format;
import static java.util.EnumSet.of;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toList;
import static org.molgenis.data.RepositoryCollectionCapability.META_DATA_PERSISTABLE;
import static org.molgenis.data.RepositoryCollectionCapability.UPDATABLE;
import static org.molgenis.data.RepositoryCollectionCapability.WRITABLE;
import static org.molgenis.data.meta.AttributeType.COMPOUND;
import static org.molgenis.data.meta.AttributeType.ENUM;
import static org.molgenis.data.meta.AttributeType.ONE_TO_MANY;
import static org.molgenis.data.meta.model.EntityTypeMetadata.BACKEND;
import static org.molgenis.data.meta.model.EntityTypeMetadata.ENTITY_TYPE_META_DATA;
import static org.molgenis.data.meta.model.EntityTypeMetadata.ID;
import static org.molgenis.data.meta.model.EntityTypeMetadata.IS_ABSTRACT;
import static org.molgenis.data.postgresql.PostgreSqlQueryGenerator.ColumnMode;
import static org.molgenis.data.postgresql.PostgreSqlQueryGenerator.generateSqlColumnDefaultConstraint;
import static org.molgenis.data.postgresql.PostgreSqlQueryGenerator.getSqlAddColumn;
import static org.molgenis.data.postgresql.PostgreSqlQueryGenerator.getSqlCreateCheckConstraint;
import static org.molgenis.data.postgresql.PostgreSqlQueryGenerator.getSqlCreateForeignKey;
import static org.molgenis.data.postgresql.PostgreSqlQueryGenerator.getSqlCreateFunctionValidateUpdate;
import static org.molgenis.data.postgresql.PostgreSqlQueryGenerator.getSqlCreateJunctionTable;
import static org.molgenis.data.postgresql.PostgreSqlQueryGenerator.getSqlCreateJunctionTableIndex;
import static org.molgenis.data.postgresql.PostgreSqlQueryGenerator.getSqlCreateTable;
import static org.molgenis.data.postgresql.PostgreSqlQueryGenerator.getSqlCreateUniqueKey;
import static org.molgenis.data.postgresql.PostgreSqlQueryGenerator.getSqlCreateUpdateTrigger;
import static org.molgenis.data.postgresql.PostgreSqlQueryGenerator.getSqlDropCheckConstraint;
import static org.molgenis.data.postgresql.PostgreSqlQueryGenerator.getSqlDropColumn;
import static org.molgenis.data.postgresql.PostgreSqlQueryGenerator.getSqlDropColumnDefault;
import static org.molgenis.data.postgresql.PostgreSqlQueryGenerator.getSqlDropForeignKey;
import static org.molgenis.data.postgresql.PostgreSqlQueryGenerator.getSqlDropFunctionValidateUpdate;
import static org.molgenis.data.postgresql.PostgreSqlQueryGenerator.getSqlDropJunctionTable;
import static org.molgenis.data.postgresql.PostgreSqlQueryGenerator.getSqlDropNotNull;
import static org.molgenis.data.postgresql.PostgreSqlQueryGenerator.getSqlDropTable;
import static org.molgenis.data.postgresql.PostgreSqlQueryGenerator.getSqlDropUniqueKey;
import static org.molgenis.data.postgresql.PostgreSqlQueryGenerator.getSqlDropUpdateTrigger;
import static org.molgenis.data.postgresql.PostgreSqlQueryGenerator.getSqlSetDataType;
import static org.molgenis.data.postgresql.PostgreSqlQueryGenerator.getSqlSetNotNull;
import static org.molgenis.data.postgresql.PostgreSqlQueryGenerator.getSqlUpdate;
import static org.molgenis.data.postgresql.PostgreSqlQueryUtils.getJunctionTableAttributes;
import static org.molgenis.data.postgresql.PostgreSqlQueryUtils.getTableAttributesReadonly;
import static org.molgenis.data.postgresql.PostgreSqlQueryUtils.isTableAttribute;
import static org.molgenis.data.postgresql.PostgreSqlRepository.BATCH_SIZE;
import static org.molgenis.data.postgresql.PostgreSqlRepository.createJunctionTableRowData;
import static org.molgenis.data.postgresql.PostgreSqlUtils.getPostgreSqlValue;
import static org.molgenis.data.util.EntityTypeUtils.getEntityTypeFetch;
import static org.molgenis.data.util.EntityTypeUtils.isMultipleReferenceType;
import static org.molgenis.data.util.EntityTypeUtils.isReferenceType;
import static org.molgenis.data.util.EntityTypeUtils.isSingleReferenceType;
import static org.molgenis.util.stream.MapCollectors.toLinkedMap;
import static org.springframework.jdbc.support.JdbcUtils.closeConnection;

import com.google.common.collect.Iterables;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.stream.Stream;
import javax.sql.DataSource;
import org.molgenis.data.AttributeValueConversionException;
import org.molgenis.data.DataService;
import org.molgenis.data.Entity;
import org.molgenis.data.Fetch;
import org.molgenis.data.MolgenisDataException;
import org.molgenis.data.Repository;
import org.molgenis.data.RepositoryCollectionCapability;
import org.molgenis.data.UnknownAttributeException;
import org.molgenis.data.UnknownRepositoryException;
import org.molgenis.data.meta.model.Attribute;
import org.molgenis.data.meta.model.EntityType;
import org.molgenis.data.support.AbstractRepositoryCollection;
import org.molgenis.data.util.AttributeUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import org.springframework.jdbc.core.JdbcTemplate;

public class PostgreSqlRepositoryCollection extends AbstractRepositoryCollection {
  private static final Logger LOG = LoggerFactory.getLogger(PostgreSqlRepositoryCollection.class);

  public static final String POSTGRESQL = "PostgreSQL";

  private final PostgreSqlEntityFactory postgreSqlEntityFactory;
  private final DataSource dataSource;
  private final JdbcTemplate jdbcTemplate;
  private final DataService dataService;

  PostgreSqlRepositoryCollection(
      PostgreSqlEntityFactory postgreSqlEntityFactory,
      DataSource dataSource,
      JdbcTemplate jdbcTemplate,
      DataService dataService) {
    this.postgreSqlEntityFactory = requireNonNull(postgreSqlEntityFactory);
    this.dataSource = requireNonNull(dataSource);
    this.jdbcTemplate = requireNonNull(jdbcTemplate);
    this.dataService = requireNonNull(dataService);
  }

  @Override
  public String getName() {
    return POSTGRESQL;
  }

  @Override
  public Set getCapabilities() {
    return immutableEnumSet(of(WRITABLE, UPDATABLE, META_DATA_PERSISTABLE));
  }

  @Override
  public boolean hasRepository(String name) {
    throw new UnsupportedOperationException();
  }

  @Override
  public Repository createRepository(EntityType entityType) {
    PostgreSqlRepository repository = createPostgreSqlRepository(entityType);
    if (!isTableExists(entityType)) {
      createTable(entityType);
    }
    return repository;
  }

  @Override
  public boolean hasRepository(EntityType entityType) {
    return isTableExists(entityType);
  }

  @Override
  public Iterable getEntityTypeIds() {
    return dataService
            .query(ENTITY_TYPE_META_DATA, EntityType.class)
            .eq(BACKEND, POSTGRESQL)
            .fetch(getEntityTypeFetch())
            .findAll()
            .map(EntityType::getId)
        ::iterator;
  }

  @Override
  public Repository getRepository(String id) {
    EntityType entityType =
        dataService
            .query(ENTITY_TYPE_META_DATA, EntityType.class)
            .eq(BACKEND, POSTGRESQL)
            .and()
            .eq(ID, id)
            .and()
            .eq(IS_ABSTRACT, false)
            .fetch(getEntityTypeFetch())
            .findOne();
    return getRepository(entityType);
  }

  @Override
  public Repository getRepository(EntityType entityType) {
    return createPostgreSqlRepository(entityType);
  }

  @Override
  public Iterator> iterator() {
    return dataService
        .query(ENTITY_TYPE_META_DATA, EntityType.class)
        .eq(BACKEND, POSTGRESQL)
        .and()
        .eq(IS_ABSTRACT, false)
        .fetch(getEntityTypeFetch())
        .findAll()
        .map(this::getRepository)
        .iterator();
  }

  @Override
  public void deleteRepository(EntityType entityType) {
    if (entityType.isAbstract()) {
      throw new UnknownRepositoryException(entityType.getId());
    }
    dropTables(entityType);
  }

  @Override
  public void updateRepository(EntityType entityType, EntityType updatedEntityType) {
    //  no actions needed
  }

  private void dropTables(EntityType entityType) {
    getJunctionTableAttributes(entityType)
        .forEach(mrefAttr -> dropJunctionTable(entityType, mrefAttr));

    String sqlDropTable = getSqlDropTable(entityType);
    if (LOG.isDebugEnabled()) {
      LOG.debug("Dropping table for entity [{}]", entityType.getId());
      if (LOG.isTraceEnabled()) {
        LOG.trace("SQL: {}", sqlDropTable);
      }
    }
    jdbcTemplate.execute(sqlDropTable);

    if (getTableAttributesReadonly(entityType).findAny().isPresent()) {
      String sqlDropFunctionValidateUpdate = getSqlDropFunctionValidateUpdate(entityType);
      if (LOG.isDebugEnabled()) {
        LOG.debug("Dropping trigger function for entity [{}]", entityType.getId());
        if (LOG.isTraceEnabled()) {
          LOG.trace("SQL: {}", sqlDropFunctionValidateUpdate);
        }
      }
      jdbcTemplate.execute(sqlDropFunctionValidateUpdate);
    }
  }

  @Override
  public void addAttribute(EntityType entityType, Attribute attr) {
    if (entityType.isAbstract()) {
      throw new MolgenisDataException(
          format(
              "Cannot add attribute [%s] to abstract entity type [%s].",
              attr.getName(), entityType.getId()));
    }
    if (entityType.getAttribute(attr.getName()) != null) {
      throw new MolgenisDataException(
          format(
              "Adding attribute operation failed. Attribute already exists [%s]", attr.getName()));
    }
    addAttributeInternal(entityType, attr);
  }

  @Override
  public void updateAttribute(EntityType entityType, Attribute attr, Attribute updatedAttr) {
    if (entityType.isAbstract()) {
      throw new MolgenisDataException(
          format(
              "Cannot update attribute [%s] for abstract entity type [%s].",
              attr.getName(), entityType.getId()));
    }
    if (!isPersisted(attr) && !isPersisted(updatedAttr)) {
      return;
    }

    if (isPersisted(attr) && !isPersisted(updatedAttr)) {
      deleteAttribute(entityType, attr);
    } else if (!isPersisted(attr) && isPersisted(updatedAttr)) {
      addAttributeInternal(entityType, updatedAttr);
    } else {
      updateColumn(entityType, attr, updatedAttr);
    }
  }

  @Override
  public void deleteAttribute(EntityType entityType, Attribute attr) {
    if (entityType.isAbstract()) {
      throw new MolgenisDataException(
          format(
              "Cannot delete attribute [%s] from abstract entity type [%s].",
              attr.getName(), entityType.getId()));
    }
    if (entityType.getAttribute(attr.getName()) == null) {
      throw new UnknownAttributeException(entityType, attr.getName());
    }
    if (!isPersisted(attr)) {
      return;
    }

    if (isMultipleReferenceType(attr)) {
      dropJunctionTable(entityType, attr);
    } else {
      dropColumn(entityType, attr);
    }
  }

  /**
   * Add attribute to entityType.
   *
   * @param entityType the {@link EntityType} to add attribute to
   * @param attr attribute to add
   */
  private void addAttributeInternal(EntityType entityType, Attribute attr) {
    if (!isPersisted(attr)) {
      return;
    }

    if (isMultipleReferenceType(attr)) {
      createJunctionTable(entityType, attr);

      if (attr.getDefaultValue() != null && !attr.isNillable()) {
        @SuppressWarnings("unchecked")
        Iterable defaultRefEntities =
            (Iterable) AttributeUtils.getDefaultTypedValue(attr);
        if (!Iterables.isEmpty(defaultRefEntities)) {
          createJunctionTableRows(entityType, attr, defaultRefEntities);
        }
      }
    } else {
      createColumn(entityType, attr);
    }
  }

  private void createJunctionTableRows(
      EntityType entityType, Attribute attr, Iterable defaultRefEntities) {
    int nrRefEntities = Iterables.size(defaultRefEntities);

    PostgreSqlRepository postgreSqlRepository = createPostgreSqlRepository(entityType);

    Attribute idAttribute = entityType.getIdAttribute();
    String idAttributeName = idAttribute.getName();
    postgreSqlRepository.forEachBatched(
        new Fetch().field(idAttributeName),
        entities -> {
          List> mrefs = new ArrayList<>(entities.size() * nrRefEntities);
          entities.forEach(
              entity -> {
                AtomicInteger seqNr = new AtomicInteger(0);
                defaultRefEntities.forEach(
                    defaultRefEntity ->
                        mrefs.add(
                            createJunctionTableRowData(
                                seqNr.getAndIncrement(),
                                idAttribute,
                                defaultRefEntity,
                                attr,
                                entity)));
              });
          postgreSqlRepository.addMrefs(mrefs, attr);
        },
        BATCH_SIZE);
  }

  /**
   * Indicates if the attribute is persisted in the database. Compound attributes, computed
   * attributes with an expression and one-to-many mappedBy attributes are not persisted.
   *
   * @param attr the attribute to check
   * @return boolean indicating if the entity is persisted in the database.
   */
  private static boolean isPersisted(Attribute attr) {
    return !attr.hasExpression()
        && attr.getDataType() != COMPOUND
        && !(attr.getDataType() == ONE_TO_MANY && attr.isMappedBy());
  }

  /**
   * Updates database column based on attribute changes.
   *
   * @param entityType entity meta data
   * @param attr current attribute
   * @param updatedAttr updated attribute
   */
  private void updateColumn(EntityType entityType, Attribute attr, Attribute updatedAttr) {
    // nullable changes
    if (!Objects.equals(attr.isNillable(), updatedAttr.isNillable())) {
      updateNillable(entityType, attr, updatedAttr);
    }

    // unique changes
    if (!Objects.equals(attr.isUnique(), updatedAttr.isUnique())) {
      updateUnique(entityType, attr, updatedAttr);
    }

    // readonly changes
    if (!Objects.equals(attr.isReadOnly(), updatedAttr.isReadOnly())) {
      updateReadonly(entityType, attr, updatedAttr);
    }

    // data type changes
    if (!Objects.equals(attr.getDataType(), updatedAttr.getDataType())) {
      if (updatedAttr.isReadOnly()) {
        dropTableTriggers(entityType);
      }
      updateDataType(entityType, attr, updatedAttr);
      if (updatedAttr.isReadOnly()) {
        createTableTriggers(entityType);
      }
    }

    // ref entity changes
    if (attr.hasRefEntity()
        && updatedAttr.hasRefEntity()
        && !attr.getRefEntity().getId().equals(updatedAttr.getRefEntity().getId())) {
      updateRefEntity(entityType, attr, updatedAttr);
    }

    // enum option changes
    if (!Objects.equals(attr.getEnumOptions(), updatedAttr.getEnumOptions())) {
      updateEnumOptions(entityType, attr, updatedAttr);
    }
  }

  /**
   * Updates foreign keys based on referenced entity changes.
   *
   * @param entityType entity meta data
   * @param attr current attribute
   * @param updatedAttr updated attribute
   */
  private void updateRefEntity(EntityType entityType, Attribute attr, Attribute updatedAttr) {
    if (isSingleReferenceType(attr) && isSingleReferenceType(updatedAttr)) {
      dropForeignKey(entityType, attr);

      if (attr.getRefEntity().getIdAttribute().getDataType()
          != updatedAttr.getRefEntity().getIdAttribute().getDataType()) {
        updateColumnDataType(entityType, updatedAttr);
      }

      createForeignKey(entityType, updatedAttr);
    } else if (isMultipleReferenceType(attr) && isMultipleReferenceType(updatedAttr)) {
      throw new MolgenisDataException(
          format(
              "Updating entity [%s] attribute [%s] referenced entity from [%s] to [%s] not allowed for type [%s]",
              entityType.getId(),
              attr.getName(),
              attr.getRefEntity().getId(),
              updatedAttr.getRefEntity().getId(),
              updatedAttr.getDataType().toString()));
    }
  }

  /**
   * Updates check constraint based on enum value changes.
   *
   * @param entityType entity meta data
   * @param attr current attribute
   * @param updatedAttr updated attribute
   */
  private void updateEnumOptions(EntityType entityType, Attribute attr, Attribute updatedAttr) {
    if (attr.getDataType() == ENUM) {
      if (updatedAttr.getDataType() == ENUM) {
        // update check constraint
        dropCheckConstraint(entityType, attr);
        createCheckConstraint(entityType, updatedAttr);
      } else {
        // drop check constraint
        dropCheckConstraint(entityType, attr);
      }
    } else {
      if (updatedAttr.getDataType() == ENUM) {
        createCheckConstraint(entityType, updatedAttr);
      }
    }
  }

  private void dropColumnDefaultValue(EntityType entityType, Attribute attr) {
    String dropColumnDefaultValueSql = getSqlDropColumnDefault(entityType, attr);
    if (LOG.isDebugEnabled()) {
      LOG.debug(
          "Dropping column default constraint for entity [{}] attribute [{}]",
          entityType.getId(),
          attr.getName());
      if (LOG.isTraceEnabled()) {
        LOG.trace("SQL: {}", dropColumnDefaultValueSql);
      }
    }
    jdbcTemplate.execute(dropColumnDefaultValueSql);
  }

  /**
   * Updates column data type and foreign key constraints based on data type update.
   *
   * @param entityType entity meta data
   * @param attr current attribute
   * @param updatedAttr updated attribute
   */
  private void updateDataType(EntityType entityType, Attribute attr, Attribute updatedAttr) {
    Attribute idAttr = entityType.getIdAttribute();
    if (idAttr != null && idAttr.getName().equals(attr.getName())) {
      throw new MolgenisDataException(
          format(
              "Data type of entity [%s] attribute [%s] cannot be modified, because [%s] is an ID attribute.",
              entityType.getId(), attr.getName(), attr.getName()));
    }

    // do nothing on representation changes XREF --> CATEGORICAL
    if (isSingleReferenceType(attr) && isSingleReferenceType(updatedAttr)) {
      return;
    }

    // do nothing on representation changes MREF --> CATEGORICAL_MREF
    if (isMultipleReferenceType(attr) && isMultipleReferenceType(updatedAttr)) {
      return;
    }

    // remove foreign key on data type updates such as XREF --> STRING
    if (isSingleReferenceType(attr) && !isReferenceType(updatedAttr)) {
      dropForeignKey(entityType, attr);
    }

    if (isSingleReferenceType(attr) && isMultipleReferenceType(updatedAttr)) {
      updateManyToOneToManyToMany(entityType, attr, updatedAttr);
    } else if (isMultipleReferenceType(attr) && isSingleReferenceType(updatedAttr)) {
      updateManyToManyToManyToOne(entityType, attr, updatedAttr);
    } else {
      updateColumnDataType(entityType, updatedAttr);
    }

    // add foreign key on data type updates such as STRING --> XREF
    if (!isReferenceType(attr) && isSingleReferenceType(updatedAttr)) {
      createForeignKey(entityType, updatedAttr);
    }
  }

  private void updateManyToManyToManyToOne(
      EntityType entityType, Attribute attr, Attribute updatedAttr) {
    if (!isMultipleReferenceType(attr)) {
      throw new IllegalArgumentException();
    }
    if (!isSingleReferenceType(updatedAttr)) {
      throw new IllegalArgumentException();
    }
    if (entityType.isAbstract()) {
      throw new IllegalArgumentException();
    }

    // 1. add table column
    boolean previousNillable = updatedAttr.isNillable();
    updatedAttr.setNillable(true);
    createColumn(entityType, updatedAttr);
    updatedAttr.setNillable(previousNillable);

    // 2. move data from junction table to table column
    Attribute idAttribute = entityType.getIdAttribute();
    Attribute refIdAttribute = attr.getRefEntity().getIdAttribute();
    Fetch fetch =
        new Fetch()
            .field(idAttribute.getName())
            .field(attr.getName(), new Fetch().field(refIdAttribute.getName()));

    String updateSql = getSqlUpdate(entityType, updatedAttr);

    PostgreSqlRepository postgreSqlRepository = createPostgreSqlRepository(entityType);
    postgreSqlRepository.forEachBatched(
        fetch,
        entitiesBatch -> {
          // validate
          entitiesBatch.forEach(
              entity -> {
                if (Iterables.size(entity.getEntities(attr.getName())) > 1) {
                  throw new AttributeValueConversionException(
                      format(
                          "Entity type '%s' attribute '%s' type '%s' can't be updated to '%s' because one or more entities refer to multiple referenced entities.",
                          entityType.getLabel(),
                          attr.getName(),
                          attr.getDataType(),
                          updatedAttr.getDataType()));
                }
              });

          // update
          if (LOG.isDebugEnabled()) {
            LOG.debug("Updating {} [{}] entities", entitiesBatch.size(), getName());
            if (LOG.isTraceEnabled()) {
              LOG.trace("SQL: {}", updateSql);
            }
          }

          jdbcTemplate.batchUpdate(
              updateSql,
              new BatchPreparedStatementSetter() {
                @Override
                public void setValues(PreparedStatement ps, int i) throws SQLException {
                  Entity entity = entitiesBatch.get(i);
                  Entity refEntity = Iterables.getFirst(entity.getEntities(attr.getName()), null);
                  Object postgreSqlValue =
                      refEntity != null
                          ? getPostgreSqlValue(
                              refEntity, refEntity.getEntityType().getIdAttribute())
                          : null;
                  ps.setObject(1, postgreSqlValue);
                  ps.setObject(2, getPostgreSqlValue(entity, idAttribute));
                }

                @Override
                public int getBatchSize() {
                  return entitiesBatch.size();
                }
              });
        },
        BATCH_SIZE);

    // 3. set nillable
    if (!previousNillable) {
      updateNillable(entityType, attr, updatedAttr);
    }

    // 4. remove junction table
    dropJunctionTable(entityType, attr);
  }

  private void updateManyToOneToManyToMany(
      EntityType entityType, Attribute attr, Attribute updatedAttr) {
    if (!isSingleReferenceType(attr)) {
      throw new IllegalArgumentException();
    }
    if (!isMultipleReferenceType(updatedAttr)) {
      throw new IllegalArgumentException();
    }
    if (entityType.isAbstract()) {
      throw new IllegalArgumentException();
    }
    if (attr.isInversedBy()) {
      throw new IllegalArgumentException();
    }

    // 1. create junction table
    createJunctionTable(entityType, updatedAttr);

    // 2. move data from table column to junction table
    Attribute idAttribute = entityType.getIdAttribute();
    Attribute refIdAttribute = attr.getRefEntity().getIdAttribute();
    Fetch fetch =
        new Fetch()
            .field(idAttribute.getName())
            .field(attr.getName(), new Fetch().field(refIdAttribute.getName()));

    PostgreSqlRepository postgreSqlRepository = createPostgreSqlRepository(entityType);
    postgreSqlRepository.forEachBatched(
        fetch,
        entities -> {
          List> mrefs = createMrefs(attr, updatedAttr, idAttribute, entities);
          postgreSqlRepository.addMrefs(mrefs, updatedAttr);
        },
        BATCH_SIZE);

    // 3. remove table column
    dropColumn(entityType, attr);
  }

  private List> createMrefs(
      Attribute attr, Attribute updatedAttr, Attribute idAttribute, List entities) {
    return entities.stream()
        .map(
            entity -> {
              Entity refEntity = entity.getEntity(attr.getName());
              return refEntity != null
                  ? createJunctionTableRowData(0, idAttribute, refEntity, updatedAttr, entity)
                  : null;
            })
        .filter(Objects::nonNull)
        .collect(toList());
  }

  /**
   * Updates unique constraint based on attribute unique changes.
   *
   * @param entityType entity meta data
   * @param attr current attribute
   * @param updatedAttr updated attribute
   */
  private void updateUnique(EntityType entityType, Attribute attr, Attribute updatedAttr) {
    if (attr.isUnique() && !updatedAttr.isUnique()) {
      Attribute idAttr = entityType.getIdAttribute();
      if (idAttr != null && idAttr.getName().equals(attr.getName())) {
        throw new MolgenisDataException(
            format(
                "ID attribute [%s] of entity [%s] must be unique",
                attr.getName(), entityType.getId()));
      }

      dropUniqueKey(entityType, updatedAttr);
    } else if (!attr.isUnique() && updatedAttr.isUnique()) {
      createUniqueKey(entityType, updatedAttr);
    }
  }

  /**
   * Updates triggers and functions based on attribute readonly changes.
   *
   * @param entityType entity meta data
   * @param attr current attribute
   * @param updatedAttr updated attribute
   */
  private void updateReadonly(EntityType entityType, Attribute attr, Attribute updatedAttr) {
    Map readonlyTableAttrs =
        getTableAttributesReadonly(entityType)
            .collect(toLinkedMap(Attribute::getName, Function.identity()));
    if (!readonlyTableAttrs.isEmpty()) {
      dropTableTriggers(entityType);
    }

    if (attr.isReadOnly() && !updatedAttr.isReadOnly()) {
      readonlyTableAttrs.remove(attr.getName());
    } else if (!attr.isReadOnly() && updatedAttr.isReadOnly()) {
      readonlyTableAttrs.put(updatedAttr.getName(), updatedAttr);
    }

    if (!readonlyTableAttrs.isEmpty()) {
      createTableTriggers(entityType, readonlyTableAttrs.values());
    }
  }

  /** Return a new PostgreSQL repository */
  private PostgreSqlRepository createPostgreSqlRepository(EntityType entityType) {
    return new PostgreSqlRepository(postgreSqlEntityFactory, jdbcTemplate, dataSource, entityType);
  }

  private boolean isTableExists(EntityType entityType) {
    return isTableExists(PostgreSqlNameGenerator.getTableName(entityType, false));
  }

  private boolean isTableExists(String tableName) {
    Connection conn = null;
    try {
      conn = dataSource.getConnection();
      DatabaseMetaData dbm = conn.getMetaData();
      // DatabaseMetaData.getTables() requires table name without double quotes, only search TABLE
      // table type to
      // avoid matches with system tables
      ResultSet tables = dbm.getTables(null, null, tableName, new String[] {"TABLE"});
      return tables.next();
    } catch (Exception e) {
      throw new RuntimeException(e);
    } finally {
      closeConnection(conn);
    }
  }

  private void updateNillable(EntityType entityType, Attribute attr, Attribute updatedAttr) {
    if (attr.isNillable() && !updatedAttr.isNillable()) {
      String sqlSetNotNull = getSqlSetNotNull(entityType, updatedAttr);
      if (LOG.isDebugEnabled()) {
        LOG.debug(
            "Creating not null constraint for entity [{}] attribute [{}]",
            entityType.getId(),
            attr.getName());
        if (LOG.isTraceEnabled()) {
          LOG.trace("SQL: {}", sqlSetNotNull);
        }
      }
      jdbcTemplate.execute(sqlSetNotNull);
    } else if (!attr.isNillable() && updatedAttr.isNillable()) {
      Attribute idAttr = entityType.getIdAttribute();
      if (idAttr != null && idAttr.getName().equals(attr.getName())) {
        throw new MolgenisDataException(
            format(
                "ID attribute [%s] of entity [%s] cannot be nullable",
                attr.getName(), entityType.getId()));
      }

      String sqlDropNotNull = getSqlDropNotNull(entityType, updatedAttr);
      if (LOG.isDebugEnabled()) {
        LOG.debug(
            "Removing not null constraint for entity [{}] attribute [{}]",
            entityType.getId(),
            attr.getName());
        if (LOG.isTraceEnabled()) {
          LOG.trace("SQL: {}", sqlDropNotNull);
        }
      }
      jdbcTemplate.execute(sqlDropNotNull);
    }
  }

  private void createTable(EntityType entityType) {
    // create table
    String createTableSql = getSqlCreateTable(entityType);
    if (LOG.isDebugEnabled()) {
      LOG.debug("Creating table for entity [{}]", entityType.getId());
      if (LOG.isTraceEnabled()) {
        LOG.trace("SQL: {}", createTableSql);
      }
    }
    jdbcTemplate.execute(createTableSql);

    createTableTriggers(entityType);

    // create junction tables for attributes referencing multiple entities
    createJunctionTables(entityType);
  }

  private void createTableTriggers(EntityType entityType) {
    List readonlyTableAttrs = getTableAttributesReadonly(entityType).collect(toList());
    if (!readonlyTableAttrs.isEmpty()) {
      createTableTriggers(entityType, readonlyTableAttrs);
    }
  }

  private void createTableTriggers(
      EntityType entityType, Collection readonlyTableAttrs) {
    String createFunctionSql = getSqlCreateFunctionValidateUpdate(entityType, readonlyTableAttrs);
    if (LOG.isDebugEnabled()) {
      LOG.debug("Creating update trigger function for entity [{}]", entityType.getId());
      if (LOG.isTraceEnabled()) {
        LOG.trace("SQL: {}", createFunctionSql);
      }
    }
    jdbcTemplate.execute(createFunctionSql);

    String createUpdateTriggerSql = getSqlCreateUpdateTrigger(entityType, readonlyTableAttrs);
    if (LOG.isDebugEnabled()) {
      LOG.debug("Creating update trigger for entity [{}]", entityType.getId());
      if (LOG.isTraceEnabled()) {
        LOG.trace("SQL: {}", createUpdateTriggerSql);
      }
    }
    jdbcTemplate.execute(createUpdateTriggerSql);
  }

  private void updateTableTriggers(
      EntityType entityType, Collection readonlyTableAttrs) {
    dropTableTriggers(entityType);
    createTableTriggers(entityType, readonlyTableAttrs);
  }

  private void dropTableTriggers(EntityType entityType) {
    String dropUpdateTriggerSql = getSqlDropUpdateTrigger(entityType);
    if (LOG.isDebugEnabled()) {
      LOG.debug("Deleting update trigger for entity [{}]", entityType.getId());
      if (LOG.isTraceEnabled()) {
        LOG.trace("SQL: {}", dropUpdateTriggerSql);
      }
    }
    jdbcTemplate.execute(dropUpdateTriggerSql);

    String dropFunctionValidateUpdateSql = getSqlDropFunctionValidateUpdate(entityType);
    if (LOG.isDebugEnabled()) {
      LOG.debug("Deleting update trigger function for entity [{}]", entityType.getId());
      if (LOG.isTraceEnabled()) {
        LOG.trace("SQL: {}", dropFunctionValidateUpdateSql);
      }
    }
    jdbcTemplate.execute(dropFunctionValidateUpdateSql);
  }

  private void createJunctionTables(EntityType entityType) {
    getJunctionTableAttributes(entityType).forEach(attr -> createJunctionTable(entityType, attr));
  }

  private void createForeignKey(EntityType entityType, Attribute attr) {
    String createForeignKeySql = getSqlCreateForeignKey(entityType, attr);
    if (LOG.isDebugEnabled()) {
      LOG.debug(
          "Creating foreign key for entity [{}] attribute [{}]",
          entityType.getId(),
          attr.getName());
      if (LOG.isTraceEnabled()) {
        LOG.trace("SQL: {}", createForeignKeySql);
      }
    }
    jdbcTemplate.execute(createForeignKeySql);
  }

  private void dropForeignKey(EntityType entityType, Attribute attr) {
    String dropForeignKeySql = getSqlDropForeignKey(entityType, attr);
    if (LOG.isDebugEnabled()) {
      LOG.debug(
          "Dropping foreign key for entity [{}] attribute [{}]",
          entityType.getId(),
          attr.getName());
      if (LOG.isTraceEnabled()) {
        LOG.trace("SQL: {}", dropForeignKeySql);
      }
    }
    jdbcTemplate.execute(dropForeignKeySql);
  }

  private void createUniqueKey(EntityType entityType, Attribute attr) {
    String createUniqueKeySql = getSqlCreateUniqueKey(entityType, attr);
    if (LOG.isDebugEnabled()) {
      LOG.debug(
          "Creating unique key for entity [{}] attribute [{}]", entityType.getId(), attr.getName());
      if (LOG.isTraceEnabled()) {
        LOG.trace("SQL: {}", createUniqueKeySql);
      }
    }
    jdbcTemplate.execute(createUniqueKeySql);
  }

  private void dropUniqueKey(EntityType entityType, Attribute attr) {
    String dropUniqueKeySql = getSqlDropUniqueKey(entityType, attr);
    if (LOG.isDebugEnabled()) {
      LOG.debug(
          "Dropping unique key for entity [{}] attribute [{}]", entityType.getId(), attr.getName());
      if (LOG.isTraceEnabled()) {
        LOG.trace("SQL: {}", dropUniqueKeySql);
      }
    }
    jdbcTemplate.execute(dropUniqueKeySql);
  }

  private void createCheckConstraint(EntityType entityType, Attribute attr) {
    String sqlCreateCheckConstraint = getSqlCreateCheckConstraint(entityType, attr);
    if (LOG.isDebugEnabled()) {
      LOG.debug(
          "Creating check constraint for entity [{}] attribute [{}]",
          entityType.getId(),
          attr.getName());
      if (LOG.isTraceEnabled()) {
        LOG.trace("SQL: {}", sqlCreateCheckConstraint);
      }
    }
    jdbcTemplate.execute(sqlCreateCheckConstraint);
  }

  private void dropCheckConstraint(EntityType entityType, Attribute attr) {
    String sqlDropCheckConstraint = getSqlDropCheckConstraint(entityType, attr);
    if (LOG.isDebugEnabled()) {
      LOG.debug(
          "Dropping check constraint for entity [{}] attribute [{}]",
          entityType.getId(),
          attr.getName());
      if (LOG.isTraceEnabled()) {
        LOG.trace("SQL: {}", sqlDropCheckConstraint);
      }
    }
    jdbcTemplate.execute(sqlDropCheckConstraint);
  }

  private void createColumn(EntityType entityType, Attribute attr) {
    String addColumnSql = getSqlAddColumn(entityType, attr, ColumnMode.INCLUDE_DEFAULT_CONSTRAINT);
    if (LOG.isDebugEnabled()) {
      LOG.debug(
          "Creating column for entity [{}] attribute [{}]", entityType.getId(), attr.getName());
      if (LOG.isTraceEnabled()) {
        LOG.trace("SQL: {}", addColumnSql);
      }
    }
    jdbcTemplate.execute(addColumnSql);

    if (generateSqlColumnDefaultConstraint(attr)) {
      dropColumnDefaultValue(entityType, attr);
    }

    if (attr.isReadOnly()) {
      Stream updatedTableAttrsReadonly;
      Stream tableAttrsReadonly = getTableAttributesReadonly(entityType);
      if (isTableAttribute(attr)) {
        updatedTableAttrsReadonly = Stream.concat(tableAttrsReadonly, Stream.of(attr));
      } else {
        updatedTableAttrsReadonly = tableAttrsReadonly;
      }
      updateTableTriggers(entityType, updatedTableAttrsReadonly.collect(toList()));
    }
  }

  private void dropColumn(EntityType entityType, Attribute attr) {
    if (attr.isReadOnly()) {
      Map updatedReadonlyTableAttrs =
          getTableAttributesReadonly(entityType)
              .collect(toLinkedMap(Attribute::getName, Function.identity()));
      updatedReadonlyTableAttrs.remove(attr.getName());

      updateTableTriggers(entityType, updatedReadonlyTableAttrs.values());
    }

    String dropColumnSql = getSqlDropColumn(entityType, attr);
    if (LOG.isDebugEnabled()) {
      LOG.debug(
          "Dropping column for entity [{}] attribute [{}]", entityType.getId(), attr.getName());
      if (LOG.isTraceEnabled()) {
        LOG.trace("SQL: {}", dropColumnSql);
      }
    }
    jdbcTemplate.execute(dropColumnSql);
  }

  private void updateColumnDataType(EntityType entityType, Attribute attr) {
    String sqlSetDataType = getSqlSetDataType(entityType, attr);
    if (LOG.isDebugEnabled()) {
      LOG.debug(
          "Changing data type of entity [{}] attribute [{}] to [{}]",
          entityType.getId(),
          attr.getName(),
          attr.getDataType());
      if (LOG.isTraceEnabled()) {
        LOG.trace("SQL: {}", sqlSetDataType);
      }
    }
    jdbcTemplate.execute(sqlSetDataType);
  }

  private void createJunctionTable(EntityType entityType, Attribute attr) {
    String createJunctionTableSql = getSqlCreateJunctionTable(entityType, attr);
    if (LOG.isDebugEnabled()) {
      LOG.debug(
          "Creating junction table for entity [{}] attribute [{}]",
          entityType.getId(),
          attr.getName());
      if (LOG.isTraceEnabled()) {
        LOG.trace("SQL: {}", createJunctionTableSql);
      }
    }
    jdbcTemplate.execute(createJunctionTableSql);

    String createJunctionTableIndexSql = getSqlCreateJunctionTableIndex(entityType, attr);
    if (LOG.isDebugEnabled()) {
      LOG.debug(
          "Creating junction table index for entity [{}] attribute [{}]",
          entityType.getId(),
          attr.getName());
      if (LOG.isTraceEnabled()) {
        LOG.trace("SQL: {}", createJunctionTableIndexSql);
      }
    }
    jdbcTemplate.execute(createJunctionTableIndexSql);
  }

  private void dropJunctionTable(EntityType entityType, Attribute mrefAttr) {
    String sqlDropJunctionTable = getSqlDropJunctionTable(entityType, mrefAttr);
    if (LOG.isDebugEnabled()) {
      LOG.debug(
          "Dropping junction table for entity [{}] attribute [{}]",
          entityType.getId(),
          mrefAttr.getName());
      if (LOG.isTraceEnabled()) {
        LOG.trace("SQL: {}", sqlDropJunctionTable);
      }
    }
    jdbcTemplate.execute(sqlDropJunctionTable);
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy