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

com.datastax.driver.core.TableMetadata Maven / Gradle / Ivy

Go to download

A driver for DataStax Enterprise (DSE) and Apache Cassandra 1.2+ clusters that works exclusively with the Cassandra Query Language version 3 (CQL3) and Cassandra's binary protocol, supporting DSE-specific features such as geospatial types, DSE Graph and DSE authentication.

There is a newer version: 2.4.0
Show newest version
/*
 * Copyright DataStax, Inc.
 *
 * This software can be used solely with DataStax Enterprise. Please consult the license at
 * http://www.datastax.com/terms/datastax-dse-driver-license-terms
 */
package com.datastax.driver.core;

import com.datastax.driver.core.utils.MoreObjects;
import com.google.common.collect.ImmutableSortedSet;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** Describes a Table. */
public class TableMetadata extends AbstractTableMetadata {

  private static final Logger logger = LoggerFactory.getLogger(TableMetadata.class);

  private static final String CF_ID_V2 = "cf_id";
  private static final String CF_ID_V3 = "id";

  private static final String KEY_VALIDATOR = "key_validator";
  private static final String COMPARATOR = "comparator";
  private static final String VALIDATOR = "default_validator";

  private static final String KEY_ALIASES = "key_aliases";
  private static final String COLUMN_ALIASES = "column_aliases";
  private static final String VALUE_ALIAS = "value_alias";

  private static final String DEFAULT_KEY_ALIAS = "key";
  private static final String DEFAULT_COLUMN_ALIAS = "column";
  private static final String DEFAULT_VALUE_ALIAS = "value";

  private static final String FLAGS = "flags";
  private static final String DENSE = "dense";
  private static final String SUPER = "super";
  private static final String COMPOUND = "compound";

  private static final String EMPTY_TYPE = "empty";

  private final Map indexes;

  private final Map views;

  private TableMetadata(
      KeyspaceMetadata keyspace,
      String name,
      UUID id,
      List partitionKey,
      List clusteringColumns,
      Map columns,
      Map indexes,
      TableOptionsMetadata options,
      List clusteringOrder,
      VersionNumber cassandraVersion) {
    super(
        keyspace,
        name,
        id,
        partitionKey,
        clusteringColumns,
        columns,
        options,
        clusteringOrder,
        cassandraVersion);
    this.indexes = indexes;
    this.views = new HashMap();
  }

  static TableMetadata build(
      KeyspaceMetadata ksm,
      Row row,
      Map rawCols,
      List indexRows,
      String nameColumn,
      VersionNumber cassandraVersion,
      Cluster cluster) {
    ProtocolVersion protocolVersion =
        cluster.getConfiguration().getProtocolOptions().getProtocolVersion();
    CodecRegistry codecRegistry = cluster.getConfiguration().getCodecRegistry();

    String name = row.getString(nameColumn);
    if (ksm.isVirtual()) {

      // This is always going to be >V3 so key validator can be null
      int partitionKeySize = findPartitionKeySize(rawCols.values(), null);
      // This is always going to be >V3 so comparator and columnAliases can be null. They are not
      // used
      int clusteringSize = findClusteringSize(null, rawCols.values(), null, cassandraVersion);
      LinkedHashMap columns = new LinkedHashMap();
      List partitionKey =
          new ArrayList(
              Collections.nCopies(partitionKeySize, null));
      List clusteringColumns =
          new ArrayList(Collections.nCopies(clusteringSize, null));
      List clusteringOrder =
          new ArrayList(
              Collections.nCopies(clusteringSize, null));
      Set otherColumns = new TreeSet(columnMetadataComparator);
      TableMetadata tm =
          new TableMetadata(
              ksm,
              name,
              new UUID(0L, 0L),
              partitionKey,
              clusteringColumns,
              columns,
              Collections.emptyMap(),
              null,
              clusteringOrder,
              cassandraVersion);

      for (ColumnMetadata.Raw rawCol : rawCols.values()) {
        DataType dataType;
        if (cassandraVersion.getMajor() >= 3) {
          dataType =
              DataTypeCqlNameParser.parse(
                  rawCol.dataType, cluster, ksm.getName(), ksm.userTypes, null, false, false);
        } else {
          dataType =
              DataTypeClassNameParser.parseOne(rawCol.dataType, protocolVersion, codecRegistry);
        }
        ColumnMetadata cm = ColumnMetadata.fromRaw(tm, rawCol, dataType);

        switch (rawCol.kind) {
          case PARTITION_KEY:
            partitionKey.set(rawCol.position, cm);
            break;
          case CLUSTERING_COLUMN:
            clusteringColumns.set(rawCol.position, cm);
            clusteringOrder.set(
                rawCol.position, rawCol.isReversed ? ClusteringOrder.DESC : ClusteringOrder.ASC);
            break;
          default:
            otherColumns.add(cm);
            break;
        }
      }

      // Order for virtual table columns should mirror that of normal tables.
      for (ColumnMetadata c : partitionKey) columns.put(c.getName(), c);
      for (ColumnMetadata c : clusteringColumns) columns.put(c.getName(), c);
      for (ColumnMetadata c : otherColumns) columns.put(c.getName(), c);
      return tm;
    }

    UUID id = null;

    if (cassandraVersion.getMajor() == 2 && cassandraVersion.getMinor() >= 1)
      id = row.getUUID(CF_ID_V2);
    else if (cassandraVersion.getMajor() > 2) id = row.getUUID(CF_ID_V3);

    DataTypeClassNameParser.ParseResult comparator = null;
    DataTypeClassNameParser.ParseResult keyValidator = null;
    List columnAliases = null;

    if (cassandraVersion.getMajor() <= 2) {
      comparator =
          DataTypeClassNameParser.parseWithComposite(
              row.getString(COMPARATOR), protocolVersion, codecRegistry);
      keyValidator =
          DataTypeClassNameParser.parseWithComposite(
              row.getString(KEY_VALIDATOR), protocolVersion, codecRegistry);
      columnAliases =
          cassandraVersion.getMajor() >= 2 || row.getString(COLUMN_ALIASES) == null
              ? Collections.emptyList()
              : SimpleJSONParser.parseStringList(row.getString(COLUMN_ALIASES));
    }

    int partitionKeySize = findPartitionKeySize(rawCols.values(), keyValidator);
    int clusteringSize;

    boolean isDense;
    boolean isCompact;
    if (cassandraVersion.getMajor() > 2) {
      Set flags = row.getSet(FLAGS, String.class);
      isDense = flags.contains(DENSE);
      boolean isSuper = flags.contains(SUPER);
      boolean isCompound = flags.contains(COMPOUND);
      isCompact = isSuper || isDense || !isCompound;
      boolean isStaticCompact = !isSuper && !isDense && !isCompound;
      if (isStaticCompact) {
        rawCols = pruneStaticCompactTableColumns(rawCols);
      }
      if (isDense) {
        rawCols = pruneDenseTableColumnsV3(rawCols);
      }
      clusteringSize =
          findClusteringSize(comparator, rawCols.values(), columnAliases, cassandraVersion);
    } else {
      assert comparator != null;
      clusteringSize =
          findClusteringSize(comparator, rawCols.values(), columnAliases, cassandraVersion);
      isDense = clusteringSize != comparator.types.size() - 1;
      if (isDense) {
        rawCols = pruneDenseTableColumnsV2(rawCols);
      }
      isCompact = isDense || !comparator.isComposite;
    }

    List partitionKey =
        new ArrayList(Collections.nCopies(partitionKeySize, null));
    List clusteringColumns =
        new ArrayList(Collections.nCopies(clusteringSize, null));
    List clusteringOrder =
        new ArrayList(Collections.nCopies(clusteringSize, null));

    // We use a linked hashmap because we will keep this in the order of a 'SELECT * FROM ...'.
    LinkedHashMap columns = new LinkedHashMap();
    LinkedHashMap indexes = new LinkedHashMap();

    TableOptionsMetadata options = null;
    try {
      options = new TableOptionsMetadata(row, isCompact, cassandraVersion);
    } catch (RuntimeException e) {
      // See ControlConnection#refreshSchema for why we'd rather not probably this further. Since
      // table options is one thing
      // that tends to change often in Cassandra, it's worth special casing this.
      logger.error(
          String.format(
              "Error parsing schema options for table %s.%s: "
                  + "Cluster.getMetadata().getKeyspace(\"%s\").getTable(\"%s\").getOptions() will return null",
              ksm.getName(), name, ksm.getName(), name),
          e);
    }

    TableMetadata tm =
        new TableMetadata(
            ksm,
            name,
            id,
            partitionKey,
            clusteringColumns,
            columns,
            indexes,
            options,
            clusteringOrder,
            cassandraVersion);

    // We use this temporary set just so non PK columns are added in lexicographical order, which is
    // the one of a
    // 'SELECT * FROM ...'
    Set otherColumns = new TreeSet(columnMetadataComparator);

    if (cassandraVersion.getMajor() < 2) {

      assert comparator != null;
      assert keyValidator != null;
      assert columnAliases != null;

      // In C* 1.2, only the REGULAR columns are in the columns schema table, so we need to add the
      // names from
      // the aliases (and make sure we handle default aliases).
      List keyAliases =
          row.getString(KEY_ALIASES) == null
              ? Collections.emptyList()
              : SimpleJSONParser.parseStringList(row.getString(KEY_ALIASES));
      for (int i = 0; i < partitionKey.size(); i++) {
        String alias =
            keyAliases.size() > i
                ? keyAliases.get(i)
                : (i == 0 ? DEFAULT_KEY_ALIAS : DEFAULT_KEY_ALIAS + (i + 1));
        partitionKey.set(i, ColumnMetadata.forAlias(tm, alias, keyValidator.types.get(i)));
      }

      for (int i = 0; i < clusteringSize; i++) {
        String alias =
            columnAliases.size() > i ? columnAliases.get(i) : DEFAULT_COLUMN_ALIAS + (i + 1);
        clusteringColumns.set(i, ColumnMetadata.forAlias(tm, alias, comparator.types.get(i)));
        clusteringOrder.set(
            i, comparator.reversed.get(i) ? ClusteringOrder.DESC : ClusteringOrder.ASC);
      }

      // if we're dense, chances are that we have a single regular "value" column with an alias
      if (isDense) {
        String alias = row.isNull(VALUE_ALIAS) ? DEFAULT_VALUE_ALIAS : row.getString(VALUE_ALIAS);
        // ...unless the table does not have any regular column, only primary key columns (JAVA-873)
        if (!alias.isEmpty()) {
          DataType type =
              DataTypeClassNameParser.parseOne(
                  row.getString(VALIDATOR), protocolVersion, codecRegistry);
          otherColumns.add(ColumnMetadata.forAlias(tm, alias, type));
        }
      }
    }

    for (ColumnMetadata.Raw rawCol : rawCols.values()) {
      DataType dataType;
      if (cassandraVersion.getMajor() >= 3) {
        dataType =
            DataTypeCqlNameParser.parse(
                rawCol.dataType, cluster, ksm.getName(), ksm.userTypes, null, false, false);
      } else {
        dataType =
            DataTypeClassNameParser.parseOne(rawCol.dataType, protocolVersion, codecRegistry);
      }
      ColumnMetadata col = ColumnMetadata.fromRaw(tm, rawCol, dataType);
      switch (rawCol.kind) {
        case PARTITION_KEY:
          partitionKey.set(rawCol.position, col);
          break;
        case CLUSTERING_COLUMN:
          clusteringColumns.set(rawCol.position, col);
          clusteringOrder.set(
              rawCol.position, rawCol.isReversed ? ClusteringOrder.DESC : ClusteringOrder.ASC);
          break;
        default:
          otherColumns.add(col);
          break;
      }

      // legacy secondary indexes (C* < 3.0)
      IndexMetadata index = IndexMetadata.fromLegacy(col, rawCol);
      if (index != null) indexes.put(index.getName(), index);
    }

    for (ColumnMetadata c : partitionKey) columns.put(c.getName(), c);
    for (ColumnMetadata c : clusteringColumns) columns.put(c.getName(), c);
    for (ColumnMetadata c : otherColumns) columns.put(c.getName(), c);

    // create secondary indexes (C* >= 3.0)
    if (indexRows != null)
      for (Row indexRow : indexRows) {
        IndexMetadata index = IndexMetadata.fromRow(tm, indexRow);
        indexes.put(index.getName(), index);
      }

    return tm;
  }

  /**
   * Upon migration from thrift to CQL, we internally create a pair of surrogate clustering/regular
   * columns for compact static tables. These columns shouldn't be exposed to the user but are
   * currently returned by C*. We also need to remove the static keyword for all other columns in
   * the table.
   */
  private static Map pruneStaticCompactTableColumns(
      Map rawCols) {
    Collection cols = rawCols.values();
    Iterator it = cols.iterator();
    while (it.hasNext()) {
      ColumnMetadata.Raw col = it.next();
      if (col.kind == ColumnMetadata.Raw.Kind.CLUSTERING_COLUMN) {
        // remove "column1 text" clustering column
        it.remove();
      } else if (col.kind == ColumnMetadata.Raw.Kind.REGULAR) {
        // remove "value blob" regular column
        it.remove();
      } else if (col.kind == ColumnMetadata.Raw.Kind.STATIC) {
        // remove "static" keyword
        col.kind = ColumnMetadata.Raw.Kind.REGULAR;
      }
    }
    return rawCols;
  }

  /**
   * Upon migration from thrift to CQL, we internally create a surrogate column "value" of type
   * EmptyType for dense tables. This column shouldn't be exposed to the user but is currently
   * returned by C*.
   */
  private static Map pruneDenseTableColumnsV3(
      Map rawCols) {
    Collection cols = rawCols.values();
    Iterator it = cols.iterator();
    while (it.hasNext()) {
      ColumnMetadata.Raw col = it.next();
      if (col.kind == ColumnMetadata.Raw.Kind.REGULAR && col.dataType.equals(EMPTY_TYPE)) {
        // remove "value empty" regular column
        it.remove();
      }
    }
    return rawCols;
  }

  private static Map pruneDenseTableColumnsV2(
      Map rawCols) {
    Collection cols = rawCols.values();
    Iterator it = cols.iterator();
    while (it.hasNext()) {
      ColumnMetadata.Raw col = it.next();
      if (col.kind == ColumnMetadata.Raw.Kind.COMPACT_VALUE && col.name.isEmpty()) {
        // remove "" blob regular COMPACT_VALUE column
        it.remove();
      }
    }
    return rawCols;
  }

  private static int findPartitionKeySize(
      Collection cols, DataTypeClassNameParser.ParseResult keyValidator) {
    // C* 1.2, 2.0, 2.1 and 2.2
    if (keyValidator != null) return keyValidator.types.size();
    // C* 3.0 onwards
    int maxId = -1;
    for (ColumnMetadata.Raw col : cols)
      if (col.kind == ColumnMetadata.Raw.Kind.PARTITION_KEY) maxId = Math.max(maxId, col.position);
    return maxId + 1;
  }

  private static int findClusteringSize(
      DataTypeClassNameParser.ParseResult comparator,
      Collection cols,
      List columnAliases,
      VersionNumber cassandraVersion) {
    // In 2.0 onwards, this is relatively easy, we just find the biggest 'position' amongst the
    // clustering columns.
    // For 1.2 however, this is slightly more subtle: we need to infer it based on whether the
    // comparator is composite or not, and whether we have
    // regular columns or not.
    if (cassandraVersion.getMajor() >= 2) {
      int maxId = -1;
      for (ColumnMetadata.Raw col : cols)
        if (col.kind == ColumnMetadata.Raw.Kind.CLUSTERING_COLUMN)
          maxId = Math.max(maxId, col.position);
      return maxId + 1;
    } else {
      int size = comparator.types.size();
      if (comparator.isComposite)
        return !comparator.collections.isEmpty()
                || (columnAliases.size() == size - 1
                    && comparator.types.get(size - 1).equals(DataType.text()))
            ? size - 1
            : size;
      else
        // We know cols only has the REGULAR ones for 1.2
        return !columnAliases.isEmpty() || cols.isEmpty() ? size : 0;
    }
  }

  /**
   * Returns metadata on a index of this table.
   *
   * @param name the name of the index to retrieve ({@code name} will be interpreted as a
   *     case-insensitive identifier unless enclosed in double-quotes, see {@link Metadata#quote}).
   * @return the metadata for the {@code name} index if it exists, or {@code null} otherwise.
   */
  public IndexMetadata getIndex(String name) {
    return indexes.get(Metadata.handleId(name));
  }

  /**
   * Returns all indexes based on this table.
   *
   * @return all indexes based on this table.
   */
  public Collection getIndexes() {
    return Collections.unmodifiableCollection(indexes.values());
  }

  /**
   * Returns metadata on a view of this table.
   *
   * @param name the name of the view to retrieve ({@code name} will be interpreted as a
   *     case-insensitive identifier unless enclosed in double-quotes, see {@link Metadata#quote}).
   * @return the metadata for the {@code name} view if it exists, or {@code null} otherwise.
   */
  public MaterializedViewMetadata getView(String name) {
    return views.get(Metadata.handleId(name));
  }

  /**
   * Returns all views based on this table.
   *
   * @return all views based on this table.
   */
  public Collection getViews() {
    return Collections.unmodifiableCollection(views.values());
  }

  void add(MaterializedViewMetadata view) {
    views.put(view.getName(), view);
  }

  /**
   * Returns a {@code String} containing CQL statements representing this table and all of its
   * derived resources, such as secondary indexes or materialized views.
   *
   * 

In other words, this method returns the statements that would allow you to recreate the * complete schema of this table, along with the secondary indexes and materialized views defined * on it, if any. * *

Note that the returned String is formatted to be human readable (for some definition of * human readable at least). * * @return the CQL statements representing this table as a {@code String}. * @see #asCQLQuery */ @Override public String exportAsString() { StringBuilder sb = new StringBuilder(); sb.append(super.exportAsString()); if (!indexes.isEmpty()) { sb.append('\n'); Iterator indexIt = indexes.values().iterator(); while (indexIt.hasNext()) { IndexMetadata index = indexIt.next(); sb.append('\n').append(index.asCQLQuery()); if (indexIt.hasNext()) { sb.append('\n'); } } } if (!views.isEmpty()) { sb.append('\n'); Iterator viewsIt = ImmutableSortedSet.orderedBy(AbstractTableMetadata.byNameComparator) .addAll(views.values()) .build() .iterator(); while (viewsIt.hasNext()) { AbstractTableMetadata view = viewsIt.next(); sb.append('\n').append(view.exportAsString()); if (viewsIt.hasNext()) { sb.append('\n'); } } } return sb.toString(); } /** * Returns a single CQL statement representing this table. * *

This method returns a single {@code CREATE TABLE} statement with the options corresponding * to this table definition. * *

Note that the returned string is a single line; the returned statement is not formatted in * any way. * * @return the {@code CREATE TABLE} statement corresponding to this table. * @see #exportAsString */ @Override public String asCQLQuery() { return super.asCQLQuery(); } @Override protected String asCQLQuery(boolean formatted) { StringBuilder sb = new StringBuilder(); if (isVirtual()) { sb.append("/* VIRTUAL "); } else { sb.append("CREATE "); } sb.append("TABLE ") .append(Metadata.quoteIfNecessary(keyspace.getName())) .append('.') .append(Metadata.quoteIfNecessary(name)) .append(" ("); if (formatted) { spaceOrNewLine(sb, true); } for (ColumnMetadata cm : columns.values()) { sb.append(cm).append(','); spaceOrNewLine(sb, formatted); } // PK sb.append("PRIMARY KEY ("); if (partitionKey.size() == 1) { sb.append(Metadata.quoteIfNecessary(partitionKey.get(0).getName())); } else { sb.append('('); boolean first = true; for (ColumnMetadata cm : partitionKey) { if (first) first = false; else sb.append(", "); sb.append(Metadata.quoteIfNecessary(cm.getName())); } sb.append(')'); } for (ColumnMetadata cm : clusteringColumns) sb.append(", ").append(Metadata.quoteIfNecessary(cm.getName())); sb.append(')'); newLine(sb, formatted); // end PK sb.append(") "); appendOptions(sb, formatted); if (isVirtual()) { sb.append(" */"); } return sb.toString(); } @Override public boolean equals(Object other) { if (other == this) return true; if (!(other instanceof TableMetadata)) return false; TableMetadata that = (TableMetadata) other; return MoreObjects.equal(this.name, that.name) && MoreObjects.equal(this.id, that.id) && MoreObjects.equal(this.partitionKey, that.partitionKey) && MoreObjects.equal(this.clusteringColumns, that.clusteringColumns) && MoreObjects.equal(this.columns, that.columns) && MoreObjects.equal(this.options, that.options) && MoreObjects.equal(this.clusteringOrder, that.clusteringOrder) && MoreObjects.equal(this.indexes, that.indexes) && MoreObjects.equal(this.views, that.views); } @Override public int hashCode() { return MoreObjects.hashCode( name, id, partitionKey, clusteringColumns, columns, options, clusteringOrder, indexes, views); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy