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

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

There is a newer version: 2.0.31
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.graphql.schema.graphqlfirst.migration;

import com.datastax.oss.driver.shaded.guava.common.annotations.VisibleForTesting;
import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap;
import graphql.GraphqlErrorException;
import io.stargate.db.schema.Column;
import io.stargate.db.schema.Keyspace;
import io.stargate.db.schema.SchemaEntity;
import io.stargate.db.schema.SecondaryIndex;
import io.stargate.db.schema.Table;
import io.stargate.db.schema.UserDefinedType;
import io.stargate.graphql.schema.graphqlfirst.migration.CassandraSchemaHelper.Difference;
import io.stargate.graphql.schema.graphqlfirst.processor.EntityModel;
import io.stargate.graphql.schema.graphqlfirst.processor.MappingModel;
import io.stargate.graphql.schema.graphqlfirst.util.DirectedGraph;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;

public class CassandraMigrator {

  private static final BiFunction
      UDT_NO_CREATE_INDEX =
          (table, index) -> {
            throw new AssertionError(
                "This should never get invoked since UDT fields can't be indexed");
          };

  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, Keyspace keyspace) {
    List queries = new ArrayList<>();
    List errors = new ArrayList<>();

    for (EntityModel entity : mappingModel.getEntities().values()) {
      switch (entity.getTarget()) {
        case TABLE:
          Table expectedTable = entity.getTableCqlSchema();
          Table actualTable = keyspace.table(entity.getCqlName());
          compute(
              expectedTable,
              actualTable,
              CassandraSchemaHelper::compare,
              CreateTableQuery::createTableAndIndexes,
              DropTableQuery::new,
              AddTableColumnQuery::new,
              CreateIndexQuery::new,
              "table",
              queries,
              errors);
          break;
        case UDT:
          UserDefinedType expectedType = entity.getUdtCqlSchema();
          UserDefinedType actualType = keyspace.userDefinedType(entity.getCqlName());
          compute(
              expectedType,
              actualType,
              CassandraSchemaHelper::compare,
              CreateUdtQuery::createUdt,
              DropUdtQuery::new,
              AddUdtFieldQuery::new,
              UDT_NO_CREATE_INDEX,
              "UDT",
              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);
  }

  @SuppressWarnings("PMD.ExcessiveParameterList")
  private  void compute(
      T expected,
      T actual,
      BiFunction> comparator,
      Function> createBuilder,
      Function dropBuilder,
      BiFunction addColumnBuilder,
      BiFunction createIndexBuilder,
      String entityType,
      List queries,
      List errors) {

    switch (strategy) {
      case USE_EXISTING:
        if (actual == null) {
          errors.add(String.format("Missing %s %s", entityType, expected.name()));
        } else {
          failIfMismatch(expected, actual, comparator, errors);
        }
        break;
      case ADD_MISSING_TABLES:
        if (actual == null) {
          queries.addAll(createBuilder.apply(expected));
        } else {
          failIfMismatch(expected, actual, comparator, errors);
        }
        break;
      case ADD_MISSING_TABLES_AND_COLUMNS:
        if (actual == null) {
          queries.addAll(createBuilder.apply(expected));
        } else {
          addMissingColumns(
              expected, actual, comparator, addColumnBuilder, createIndexBuilder, queries, errors);
        }
        break;
      case DROP_AND_RECREATE_ALL:
        queries.add(dropBuilder.apply(expected));
        queries.addAll(createBuilder.apply(expected));
        break;
      case DROP_AND_RECREATE_IF_MISMATCH:
        if (actual == null) {
          queries.addAll(createBuilder.apply(expected));
        } else {
          List differences = comparator.apply(expected, actual);
          if (!differences.isEmpty()) {
            queries.add(dropBuilder.apply(expected));
            queries.addAll(createBuilder.apply(expected));
          }
        }
        break;
      default:
        throw new AssertionError("Unexpected strategy " + strategy);
    }
  }

  private  void failIfMismatch(
      T expected, T actual, BiFunction> comparator, List errors) {
    List differences = comparator.apply(expected, actual);
    if (!differences.isEmpty()) {
      errors.addAll(
          differences.stream().map(Difference::toGraphqlMessage).collect(Collectors.toList()));
    }
  }

  private  void addMissingColumns(
      T expected,
      T actual,
      BiFunction> comparator,
      BiFunction addColumnBuilder,
      BiFunction createIndexBuilder,
      List queries,
      List errors) {
    List differences = comparator.apply(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(addColumnBuilder.apply(expected, difference.getColumn()));
          } else if (difference.getType() == CassandraSchemaHelper.DifferenceType.MISSING_INDEX) {
            queries.add(createIndexBuilder.apply(expected, 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().isPartitionKey()
        && !difference.getColumn().isClusteringKey();
  }

  @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 - 2025 Weber Informatics LLC | Privacy Policy