content: /*
   * Licensed to the Apache Software Foundation (ASF) under one
   * or more contributor license agreements.  See the NOTICE file
   * distributed with this work for additional information
   * regarding copyright ownership.  The ASF licenses this file
   * to you 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 com.datastax.dse.driver.internal.core.graph.schema.refresh;
  
  import static org.assertj.core.api.Assertions.assertThat;
  import static org.mockito.Mockito.when;
  
  import com.datastax.dse.driver.api.core.metadata.schema.DseEdgeMetadata;
  import com.datastax.dse.driver.api.core.metadata.schema.DseGraphTableMetadata;
  import com.datastax.dse.driver.api.core.metadata.schema.DseVertexMetadata;
  import com.datastax.dse.driver.internal.core.metadata.schema.DefaultDseEdgeMetadata;
  import com.datastax.dse.driver.internal.core.metadata.schema.DefaultDseKeyspaceMetadata;
  import com.datastax.dse.driver.internal.core.metadata.schema.DefaultDseTableMetadata;
  import com.datastax.dse.driver.internal.core.metadata.schema.DefaultDseVertexMetadata;
  import com.datastax.oss.driver.api.core.CqlIdentifier;
  import com.datastax.oss.driver.api.core.metadata.schema.ColumnMetadata;
  import com.datastax.oss.driver.api.core.metadata.schema.TableMetadata;
  import com.datastax.oss.driver.api.core.type.DataTypes;
  import com.datastax.oss.driver.internal.core.channel.ChannelFactory;
  import com.datastax.oss.driver.internal.core.context.InternalDriverContext;
  import com.datastax.oss.driver.internal.core.metadata.DefaultMetadata;
  import com.datastax.oss.driver.internal.core.metadata.MetadataRefresh;
  import com.datastax.oss.driver.internal.core.metadata.schema.DefaultColumnMetadata;
  import com.datastax.oss.driver.internal.core.metadata.schema.events.KeyspaceChangeEvent;
  import com.datastax.oss.driver.internal.core.metadata.schema.events.TableChangeEvent;
  import com.datastax.oss.driver.internal.core.metadata.schema.refresh.SchemaRefresh;
  import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList;
  import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap;
  import edu.umd.cs.findbugs.annotations.NonNull;
  import edu.umd.cs.findbugs.annotations.Nullable;
  import java.util.Collections;
  import java.util.Map;
  import org.junit.Before;
  import org.junit.Test;
  import org.junit.runner.RunWith;
  import org.mockito.Mock;
  import org.mockito.junit.MockitoJUnitRunner;
  
  @RunWith(MockitoJUnitRunner.class)
  public class GraphSchemaRefreshTest {
  
    private static final DefaultDseTableMetadata OLD_TABLE =
        newTable(
            CqlIdentifier.fromInternal("ks_with_engine"),
            CqlIdentifier.fromInternal("tbl"),
            null,
            null);
    private static final DefaultDseKeyspaceMetadata OLD_KS1 = newKeyspace("ks1", null);
    private static final DefaultDseKeyspaceMetadata KS_WITH_ENGINE =
        newKeyspace(
            CqlIdentifier.fromInternal("ks_with_engine"),
            "Core",
            ImmutableMap.of(CqlIdentifier.fromInternal("tbl"), OLD_TABLE));
  
    @Mock private InternalDriverContext context;
    @Mock private ChannelFactory channelFactory;
    private DefaultMetadata oldMetadata;
  
    @Before
    public void setup() {
      when(context.getChannelFactory()).thenReturn(channelFactory);
      oldMetadata =
          DefaultMetadata.EMPTY.withSchema(
              ImmutableMap.of(OLD_KS1.getName(), OLD_KS1, KS_WITH_ENGINE.getName(), KS_WITH_ENGINE),
              false,
              context);
    }
  
    @Test
    public void should_detect_created_keyspace_without_graph_engine() {
      DefaultDseKeyspaceMetadata ks2 = newKeyspace("ks2", null);
      SchemaRefresh refresh =
          new SchemaRefresh(
              ImmutableMap.of(
                  OLD_KS1.getName(),
                  OLD_KS1,
                  KS_WITH_ENGINE.getName(),
                  KS_WITH_ENGINE,
                  ks2.getName(),
                  ks2));
      MetadataRefresh.Result result = refresh.compute(oldMetadata, false, context);
      assertThat(result.newMetadata.getKeyspaces()).hasSize(3);
      assertThat(result.events).containsExactly(KeyspaceChangeEvent.created(ks2));
    }
  
    @Test
    public void should_detect_created_keyspace_with_graph_engine() {
      DefaultDseKeyspaceMetadata ks2 = newKeyspace("ks2", "Core");
      SchemaRefresh refresh =
          new SchemaRefresh(
              ImmutableMap.of(
                  OLD_KS1.getName(),
                  OLD_KS1,
                  KS_WITH_ENGINE.getName(),
                  KS_WITH_ENGINE,
                  ks2.getName(),
                  ks2));
      MetadataRefresh.Result result = refresh.compute(oldMetadata, false, context);
      assertThat(result.newMetadata.getKeyspaces()).hasSize(3);
      assertThat(result.events).containsExactly(KeyspaceChangeEvent.created(ks2));
    }
  
    @Test
    public void should_detect_top_level_graph_engine_update_in_keyspace() {
      // Change only one top-level option (graph_engine)
      DefaultDseKeyspaceMetadata newKs1 = newKeyspace("ks1", "Core");
      SchemaRefresh refresh =
          new SchemaRefresh(
              ImmutableMap.of(KS_WITH_ENGINE.getName(), KS_WITH_ENGINE, OLD_KS1.getName(), newKs1));
      MetadataRefresh.Result result = refresh.compute(oldMetadata, false, context);
      assertThat(result.newMetadata.getKeyspaces()).hasSize(2);
      assertThat(result.events).containsExactly(KeyspaceChangeEvent.updated(OLD_KS1, newKs1));
    }
  
    @Test
    public void should_detect_adding_and_renaming_and_removing_vertex_label() {
      DefaultDseTableMetadata newTable =
          newTable(
              KS_WITH_ENGINE.getName(),
              CqlIdentifier.fromInternal("tbl"),
              new DefaultDseVertexMetadata(CqlIdentifier.fromInternal("someLabel")),
              null);
      DefaultDseKeyspaceMetadata ks =
          newKeyspace(
              KS_WITH_ENGINE.getName(),
              "Core",
              ImmutableMap.of(CqlIdentifier.fromInternal("tbl"), newTable));
      SchemaRefresh refresh =
          new SchemaRefresh(
              ImmutableMap.of(KS_WITH_ENGINE.getName(), ks, OLD_KS1.getName(), OLD_KS1));
      MetadataRefresh.Result result = refresh.compute(oldMetadata, false, context);
      assertThat(result.newMetadata.getKeyspaces()).hasSize(2);
      assertThat(result.events).containsExactly(TableChangeEvent.updated(OLD_TABLE, newTable));
      assertThat(result.newMetadata.getKeyspaces().get(KS_WITH_ENGINE.getName())).isNotNull();
      assertThat(
              ((DseGraphTableMetadata)
                      result
                          .newMetadata
                          .getKeyspaces()
                          .get(KS_WITH_ENGINE.getName())
                          .getTable("tbl")
                          .get())
                  .getVertex())
          .isNotNull();
      assertThat(
              ((DseGraphTableMetadata)
                      result
                          .newMetadata
                          .getKeyspaces()
                          .get(KS_WITH_ENGINE.getName())
                          .getTable("tbl")
                          .get())
                  .getVertex()
                  .get()
                  .getLabelName()
                  .asInternal())
          .isEqualTo("someLabel");
  
      // now rename the vertex label
      newTable =
          newTable(
              KS_WITH_ENGINE.getName(),
              CqlIdentifier.fromInternal("tbl"),
              new DefaultDseVertexMetadata(CqlIdentifier.fromInternal("someNewLabel")),
              null);
      ks =
          newKeyspace(
              KS_WITH_ENGINE.getName(),
              "Core",
              ImmutableMap.of(CqlIdentifier.fromInternal("tbl"), newTable));
      refresh =
          new SchemaRefresh(
              ImmutableMap.of(KS_WITH_ENGINE.getName(), ks, OLD_KS1.getName(), OLD_KS1));
      result = refresh.compute(oldMetadata, false, context);
      assertThat(result.newMetadata.getKeyspaces()).hasSize(2);
      assertThat(result.events).containsExactly(TableChangeEvent.updated(OLD_TABLE, newTable));
      assertThat(
              ((DseGraphTableMetadata)
                      result
                          .newMetadata
                          .getKeyspaces()
                          .get(KS_WITH_ENGINE.getName())
                          .getTable("tbl")
                          .get())
                  .getVertex()
                  .get()
                  .getLabelName()
                  .asInternal())
          .isEqualTo("someNewLabel");
  
      // now remove the vertex label from the table
      DefaultMetadata metadataWithVertexLabel = result.newMetadata;
      DefaultDseTableMetadata tableWithRemovedLabel =
          newTable(KS_WITH_ENGINE.getName(), CqlIdentifier.fromInternal("tbl"), null, null);
      ks =
          newKeyspace(
              KS_WITH_ENGINE.getName(),
              "Core",
              ImmutableMap.of(CqlIdentifier.fromInternal("tbl"), tableWithRemovedLabel));
      refresh =
          new SchemaRefresh(
              ImmutableMap.of(KS_WITH_ENGINE.getName(), ks, OLD_KS1.getName(), OLD_KS1));
      result = refresh.compute(metadataWithVertexLabel, false, context);
      assertThat(result.newMetadata.getKeyspaces()).hasSize(2);
      assertThat(result.events)
          .containsExactly(TableChangeEvent.updated(newTable, tableWithRemovedLabel));
      assertThat(
              ((DseGraphTableMetadata)
                      result
                          .newMetadata
                          .getKeyspaces()
                          .get(KS_WITH_ENGINE.getName())
                          .getTable("tbl")
                          .get())
                  .getVertex()
                  .isPresent())
          .isFalse();
    }
  
    @Test
    public void should_detect_adding_and_renaming_and_removing_edge_label() {
      DefaultDseTableMetadata newTable =
          newTable(
              KS_WITH_ENGINE.getName(),
              CqlIdentifier.fromInternal("tbl"),
              null,
              newEdgeMetadata(
                  CqlIdentifier.fromInternal("created"),
                  CqlIdentifier.fromInternal("person"),
                  CqlIdentifier.fromInternal("software")));
      DefaultDseKeyspaceMetadata ks =
          newKeyspace(
              KS_WITH_ENGINE.getName(),
              "Core",
              ImmutableMap.of(CqlIdentifier.fromInternal("tbl"), newTable));
      SchemaRefresh refresh =
          new SchemaRefresh(
              ImmutableMap.of(KS_WITH_ENGINE.getName(), ks, OLD_KS1.getName(), OLD_KS1));
      MetadataRefresh.Result result = refresh.compute(oldMetadata, false, context);
      assertThat(result.newMetadata.getKeyspaces()).hasSize(2);
      assertThat(result.events).containsExactly(TableChangeEvent.updated(OLD_TABLE, newTable));
      assertThat(result.newMetadata.getKeyspaces().get(KS_WITH_ENGINE.getName())).isNotNull();
      assertThat(
              ((DseGraphTableMetadata)
                      result
                          .newMetadata
                          .getKeyspaces()
                          .get(KS_WITH_ENGINE.getName())
                          .getTable("tbl")
                          .get())
                  .getVertex())
          .isNotNull();
      assertThat(
              ((DseGraphTableMetadata)
                      result
                          .newMetadata
                          .getKeyspaces()
                          .get(KS_WITH_ENGINE.getName())
                          .getTable("tbl")
                          .get())
                  .getEdge()
                  .get()
                  .getLabelName()
                  .asInternal())
          .isEqualTo("created");
  
      // now rename the edge label
      newTable =
          newTable(
              KS_WITH_ENGINE.getName(),
              CqlIdentifier.fromInternal("tbl"),
              null,
              newEdgeMetadata(
                  CqlIdentifier.fromInternal("CHANGED"),
                  CqlIdentifier.fromInternal("person"),
                  CqlIdentifier.fromInternal("software")));
      ks =
          newKeyspace(
              KS_WITH_ENGINE.getName(),
              "Core",
              ImmutableMap.of(CqlIdentifier.fromInternal("tbl"), newTable));
      refresh =
          new SchemaRefresh(
              ImmutableMap.of(KS_WITH_ENGINE.getName(), ks, OLD_KS1.getName(), OLD_KS1));
      result = refresh.compute(oldMetadata, false, context);
      assertThat(result.newMetadata.getKeyspaces()).hasSize(2);
      assertThat(result.events).containsExactly(TableChangeEvent.updated(OLD_TABLE, newTable));
      assertThat(
              ((DseGraphTableMetadata)
                      result
                          .newMetadata
                          .getKeyspaces()
                          .get(KS_WITH_ENGINE.getName())
                          .getTable("tbl")
                          .get())
                  .getEdge()
                  .get()
                  .getLabelName()
                  .asInternal())
          .isEqualTo("CHANGED");
  
      // now remove the edge label from the table
      DefaultMetadata metadataWithEdgeLabel = result.newMetadata;
      DefaultDseTableMetadata tableWithRemovedLabel =
          newTable(KS_WITH_ENGINE.getName(), CqlIdentifier.fromInternal("tbl"), null, null);
      ks =
          newKeyspace(
              KS_WITH_ENGINE.getName(),
              "Core",
              ImmutableMap.of(CqlIdentifier.fromInternal("tbl"), tableWithRemovedLabel));
      refresh =
          new SchemaRefresh(
              ImmutableMap.of(KS_WITH_ENGINE.getName(), ks, OLD_KS1.getName(), OLD_KS1));
      result = refresh.compute(metadataWithEdgeLabel, false, context);
      assertThat(result.newMetadata.getKeyspaces()).hasSize(2);
      assertThat(result.events)
          .containsExactly(TableChangeEvent.updated(newTable, tableWithRemovedLabel));
      assertThat(
              ((DseGraphTableMetadata)
                      result
                          .newMetadata
                          .getKeyspaces()
                          .get(KS_WITH_ENGINE.getName())
                          .getTable("tbl")
                          .get())
                  .getEdge()
                  .isPresent())
          .isFalse();
    }
  
    private static DefaultDseKeyspaceMetadata newKeyspace(String name, String graphEngine) {
      return new DefaultDseKeyspaceMetadata(
          CqlIdentifier.fromInternal(name),
          false,
          false,
          graphEngine,
          Collections.emptyMap(),
          Collections.emptyMap(),
          Collections.emptyMap(),
          Collections.emptyMap(),
          Collections.emptyMap(),
          Collections.emptyMap());
    }
  
    private static DefaultDseKeyspaceMetadata newKeyspace(
        CqlIdentifier name, String graphEngine, @NonNull Map<CqlIdentifier, TableMetadata> tables) {
      return new DefaultDseKeyspaceMetadata(
          name,
          false,
          false,
          graphEngine,
          Collections.emptyMap(),
          Collections.emptyMap(),
          tables,
          Collections.emptyMap(),
          Collections.emptyMap(),
          Collections.emptyMap());
    }
  
    private static DefaultDseTableMetadata newTable(
        @NonNull CqlIdentifier keyspace,
        @NonNull CqlIdentifier name,
        @Nullable DseVertexMetadata vertex,
        @Nullable DseEdgeMetadata edge) {
      ImmutableList<ColumnMetadata> cols =
          ImmutableList.of(
              new DefaultColumnMetadata(
                  keyspace,
                  CqlIdentifier.fromInternal("parent"),
                  CqlIdentifier.fromInternal("id"),
                  DataTypes.INT,
                  false));
      return new DefaultDseTableMetadata(
          keyspace,
          name,
          null,
          false,
          false,
          cols,
          Collections.emptyMap(),
          Collections.emptyMap(),
          Collections.emptyMap(),
          Collections.emptyMap(),
          vertex,
          edge);
    }
  
    private static DefaultDseEdgeMetadata newEdgeMetadata(
        @NonNull CqlIdentifier labelName,
        @NonNull CqlIdentifier fromTable,
        @NonNull CqlIdentifier toTable) {
      return new DefaultDseEdgeMetadata(
          labelName,
          fromTable,
          fromTable,
          Collections.emptyList(),
          Collections.emptyList(),
          toTable,
          toTable,
          Collections.emptyList(),
          Collections.emptyList());
    }
  }
  