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

io.stargate.sgv2.graphql.schema.graphqlfirst.migration.CassandraMigrator Maven / Gradle / Ivy

There is a newer version: 2.0.0-ALPHA-17
Show newest version
/*
 * Copyright The Stargate Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.stargate.sgv2.graphql.schema.graphqlfirst.migration;

import com.google.common.collect.ImmutableMap;
import graphql.GraphqlErrorException;
import graphql.VisibleForTesting;
import io.stargate.bridge.proto.QueryOuterClass.TypeSpec.Udt;
import io.stargate.bridge.proto.Schema.CqlKeyspaceDescribe;
import io.stargate.bridge.proto.Schema.CqlTable;
import io.stargate.sgv2.common.cql.builder.Column;
import io.stargate.sgv2.graphql.schema.graphqlfirst.migration.CassandraSchemaHelper.Difference;
import io.stargate.sgv2.graphql.schema.graphqlfirst.processor.EntityModel;
import io.stargate.sgv2.graphql.schema.graphqlfirst.processor.MappingModel;
import io.stargate.sgv2.graphql.schema.graphqlfirst.util.DirectedGraph;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

public class CassandraMigrator {

  private final MigrationStrategy strategy;
  private final boolean isPersisted;

  /** Creates a new instance for a deployment initiated by the user. */
  public static CassandraMigrator forDeployment(MigrationStrategy strategy) {
    return new CassandraMigrator(strategy, false);
  }

  /**
   * Creates a new instance for a GraphQL schema already stored in the database (from a previous
   * deployment).
   */
  public static CassandraMigrator forPersisted() {
    return new CassandraMigrator(MigrationStrategy.USE_EXISTING, true);
  }

  private CassandraMigrator(MigrationStrategy strategy, boolean isPersisted) {
    this.strategy = strategy;
    this.isPersisted = isPersisted;
  }

  /** @throws GraphqlErrorException if the CQL data model can't be migrated */
  public List compute(MappingModel mappingModel, CqlKeyspaceDescribe keyspace) {
    List queries = new ArrayList<>();
    List errors = new ArrayList<>();

    for (EntityModel entity : mappingModel.getEntities().values()) {
      switch (entity.getTarget()) {
        case TABLE:
          CqlTable expectedTable = entity.getTableCqlSchema();
          Optional actualTable =
              keyspace.getTablesList().stream()
                  .filter(t -> t.getName().equals(entity.getCqlName()))
                  .findFirst();
          compute(expectedTable, actualTable, keyspace.getCqlKeyspace().getName(), queries, errors);
          break;
        case UDT:
          Udt expectedType = entity.getUdtCqlSchema();
          Optional actualType =
              keyspace.getTypesList().stream()
                  .filter(t -> t.getName().equals(entity.getCqlName()))
                  .findFirst();
          compute(expectedType, actualType, keyspace.getCqlKeyspace().getName(), queries, errors);
          break;
        default:
          throw new AssertionError("Unexpected target " + entity.getTarget());
      }
    }
    if (!errors.isEmpty()) {
      String message =
          isPersisted
              ? "The GraphQL schema stored for this keyspace doesn't match the CQL data model "
                  + "anymore. It looks like the database was altered manually."
              : String.format(
                  "The GraphQL schema that you provided can't be mapped to the current CQL data "
                      + "model. Consider using a different migration strategy (current: %s).",
                  strategy);
      throw GraphqlErrorException.newErrorException()
          .message(message + " See details in `extensions.migrationErrors` below.")
          .extensions(ImmutableMap.of("migrationErrors", errors))
          .build();
    }
    return sortForExecution(queries);
  }

  private void compute(
      CqlTable expected,
      Optional maybeActual,
      String keyspaceName,
      List queries,
      List errors) {
    switch (strategy) {
      case USE_EXISTING:
        if (maybeActual.isPresent()) {
          failIfMismatch(expected, maybeActual.get(), errors);
        } else {
          errors.add(String.format("Missing table %s", expected.getName()));
        }
        break;
      case ADD_MISSING_TABLES:
        if (maybeActual.isPresent()) {
          failIfMismatch(expected, maybeActual.get(), errors);
        } else {
          queries.addAll(CreateTableQuery.createTableAndIndexes(keyspaceName, expected));
        }
        break;
      case ADD_MISSING_TABLES_AND_COLUMNS:
        if (maybeActual.isPresent()) {
          addMissingColumns(expected, maybeActual.get(), keyspaceName, queries, errors);
        } else {
          queries.addAll(CreateTableQuery.createTableAndIndexes(keyspaceName, expected));
        }
        break;
      case DROP_AND_RECREATE_ALL:
        queries.add(new DropTableQuery(keyspaceName, expected));
        queries.addAll(CreateTableQuery.createTableAndIndexes(keyspaceName, expected));
        break;
      case DROP_AND_RECREATE_IF_MISMATCH:
        if (maybeActual.isPresent()) {
          List differences = CassandraSchemaHelper.compare(expected, maybeActual.get());
          if (!differences.isEmpty()) {
            queries.add(new DropTableQuery(keyspaceName, expected));
            queries.addAll(CreateTableQuery.createTableAndIndexes(keyspaceName, expected));
          }
        } else {
          queries.addAll(CreateTableQuery.createTableAndIndexes(keyspaceName, expected));
        }
        break;
      default:
        throw new AssertionError("Unexpected strategy " + strategy);
    }
  }

  private void failIfMismatch(CqlTable expected, CqlTable actual, List errors) {
    List differences = CassandraSchemaHelper.compare(expected, actual);
    if (!differences.isEmpty()) {
      errors.addAll(
          differences.stream().map(Difference::toGraphqlMessage).collect(Collectors.toList()));
    }
  }

  private void addMissingColumns(
      CqlTable expected,
      CqlTable actual,
      String keyspaceName,
      List queries,
      List errors) {
    List differences = CassandraSchemaHelper.compare(expected, actual);
    if (!differences.isEmpty()) {
      List blockers =
          differences.stream().filter(d -> !isAddableColumn(d)).collect(Collectors.toList());
      if (blockers.isEmpty()) {
        for (Difference difference : differences) {
          assert isAddableColumn(difference);
          if (difference.getType() == CassandraSchemaHelper.DifferenceType.MISSING_COLUMN) {
            queries.add(
                new AddTableColumnQuery(keyspaceName, expected.getName(), difference.getColumn()));
          } else if (difference.getType() == CassandraSchemaHelper.DifferenceType.MISSING_INDEX) {
            queries.add(
                new CreateIndexQuery(keyspaceName, expected.getName(), difference.getIndex()));
          }
        }
      } else {
        errors.addAll(
            blockers.stream().map(Difference::toGraphqlMessage).collect(Collectors.toList()));
      }
    }
  }

  private boolean isAddableColumn(Difference difference) {
    return (difference.getType() == CassandraSchemaHelper.DifferenceType.MISSING_COLUMN
            || difference.getType() == CassandraSchemaHelper.DifferenceType.MISSING_INDEX)
        && difference.getColumn().getKind() != Column.Kind.PARTITION_KEY
        && difference.getColumn().getKind() != Column.Kind.CLUSTERING;
  }

  private void compute(
      Udt expected,
      Optional maybeActual,
      String keyspaceName,
      List queries,
      List errors) {

    switch (strategy) {
      case USE_EXISTING:
        if (maybeActual.isPresent()) {
          failIfMismatch(expected, maybeActual.get(), errors);
        } else {
          errors.add(String.format("Missing UDT %s", expected.getName()));
        }
        break;
      case ADD_MISSING_TABLES:
        if (maybeActual.isPresent()) {
          failIfMismatch(expected, maybeActual.get(), errors);
        } else {
          queries.add(new CreateUdtQuery(keyspaceName, expected));
        }
        break;
      case ADD_MISSING_TABLES_AND_COLUMNS:
        if (maybeActual.isPresent()) {
          addMissingColumns(expected, maybeActual.get(), keyspaceName, queries, errors);
        } else {
          queries.add(new CreateUdtQuery(keyspaceName, expected));
        }
        break;
      case DROP_AND_RECREATE_ALL:
        queries.add(new DropUdtQuery(keyspaceName, expected));
        queries.add(new CreateUdtQuery(keyspaceName, expected));
        break;
      case DROP_AND_RECREATE_IF_MISMATCH:
        if (maybeActual.isPresent()) {
          List differences = CassandraSchemaHelper.compare(expected, maybeActual.get());
          if (!differences.isEmpty()) {
            queries.add(new DropUdtQuery(keyspaceName, expected));
            queries.add(new CreateUdtQuery(keyspaceName, expected));
          }
        } else {
          queries.add(new CreateUdtQuery(keyspaceName, expected));
        }
        break;
      default:
        throw new AssertionError("Unexpected strategy " + strategy);
    }
  }

  private void failIfMismatch(Udt expected, Udt actual, List errors) {
    List differences = CassandraSchemaHelper.compare(expected, actual);
    if (!differences.isEmpty()) {
      errors.addAll(
          differences.stream().map(Difference::toGraphqlMessage).collect(Collectors.toList()));
    }
  }

  private void addMissingColumns(
      Udt expected,
      Udt actual,
      String keyspaceName,
      List queries,
      List errors) {
    List differences = CassandraSchemaHelper.compare(expected, actual);
    if (!differences.isEmpty()) {
      List blockers =
          differences.stream().filter(d -> !isAddableColumn(d)).collect(Collectors.toList());
      if (blockers.isEmpty()) {
        for (Difference difference : differences) {
          assert isAddableColumn(difference);
          if (difference.getType() == CassandraSchemaHelper.DifferenceType.MISSING_COLUMN) {
            queries.add(
                new AddUdtFieldQuery(
                    keyspaceName,
                    expected.getName(),
                    difference.getColumn().getSpec().getName(),
                    difference.getColumn().getSpec().getType()));
          }
        }
      } else {
        errors.addAll(
            blockers.stream().map(Difference::toGraphqlMessage).collect(Collectors.toList()));
      }
    }
  }

  @VisibleForTesting
  static List sortForExecution(List queries) {
    if (queries.size() < 2) {
      return queries;
    }
    DirectedGraph graph = new DirectedGraph<>(queries);
    for (MigrationQuery query1 : queries) {
      for (MigrationQuery query2 : queries) {
        @SuppressWarnings("PMD.CompareObjectsWithEquals")
        boolean isSame = query1 != query2;
        if (isSame && query1.mustRunBefore(query2)) {
          graph.addEdge(query1, query2);
        }
      }
    }
    return graph.topologicalSort();
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy