/* * 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 com.facebook.presto.cassandra; import com.facebook.presto.cassandra.util.CassandraCqlUtils; import com.facebook.presto.spi.ColumnHandle; import com.facebook.presto.spi.ColumnMetadata; import com.facebook.presto.spi.ConnectorNewTableLayout; import com.facebook.presto.spi.ConnectorOutputTableHandle; import com.facebook.presto.spi.ConnectorSession; import com.facebook.presto.spi.ConnectorTableHandle; import com.facebook.presto.spi.ConnectorTableLayout; import com.facebook.presto.spi.ConnectorTableLayoutHandle; import com.facebook.presto.spi.ConnectorTableLayoutResult; import com.facebook.presto.spi.ConnectorTableMetadata; import com.facebook.presto.spi.Constraint; import com.facebook.presto.spi.NotFoundException; import com.facebook.presto.spi.PrestoException; import com.facebook.presto.spi.SchemaNotFoundException; import com.facebook.presto.spi.SchemaTableName; import com.facebook.presto.spi.SchemaTablePrefix; import com.facebook.presto.spi.connector.ConnectorMetadata; import com.facebook.presto.spi.connector.ConnectorOutputMetadata; import com.facebook.presto.spi.predicate.TupleDomain; import com.facebook.presto.spi.type.Type; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import io.airlift.json.JsonCodec; import io.airlift.slice.Slice; import javax.inject.Inject; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import static com.facebook.presto.cassandra.CassandraType.toCassandraType; import static com.facebook.presto.spi.StandardErrorCode.NOT_SUPPORTED; import static com.facebook.presto.spi.StandardErrorCode.PERMISSION_DENIED; import static com.google.common.base.MoreObjects.toStringHelper; import static com.google.common.base.Preconditions.checkArgument; import static java.util.Locale.ENGLISH; import static java.util.Objects.requireNonNull; import static java.util.stream.Collectors.toList; public class CassandraMetadata implements ConnectorMetadata { private final String connectorId; private final CachingCassandraSchemaProvider schemaProvider; private final CassandraSession cassandraSession; private final CassandraPartitionManager partitionManager; private final boolean allowDropTable; private final JsonCodec<List<ExtraColumnMetadata>> extraColumnMetadataCodec; @Inject public CassandraMetadata(CassandraConnectorId connectorId, CachingCassandraSchemaProvider schemaProvider, CassandraSession cassandraSession, CassandraPartitionManager partitionManager, JsonCodec<List<ExtraColumnMetadata>> extraColumnMetadataCodec, CassandraClientConfig config) { this.connectorId = requireNonNull(connectorId, "connectorId is null").toString(); this.schemaProvider = requireNonNull(schemaProvider, "schemaProvider is null"); this.partitionManager = requireNonNull(partitionManager, "partitionManager is null"); this.cassandraSession = requireNonNull(cassandraSession, "cassandraSession is null"); this.allowDropTable = requireNonNull(config, "config is null").getAllowDropTable(); this.extraColumnMetadataCodec = requireNonNull(extraColumnMetadataCodec, "extraColumnMetadataCodec is null"); } @Override public List<String> listSchemaNames(ConnectorSession session) { return schemaProvider.getAllSchemas(); } @Override public CassandraTableHandle getTableHandle(ConnectorSession session, SchemaTableName tableName) { requireNonNull(tableName, "tableName is null"); try { CassandraTableHandle tableHandle = schemaProvider.getTableHandle(tableName); schemaProvider.getTable(tableHandle); return tableHandle; } catch (NotFoundException e) { // table was not found return null; } } private static SchemaTableName getTableName(ConnectorTableHandle tableHandle) { return ((CassandraTableHandle) tableHandle).getSchemaTableName(); } @Override public ConnectorTableMetadata getTableMetadata(ConnectorSession session, ConnectorTableHandle tableHandle) { requireNonNull(tableHandle, "tableHandle is null"); SchemaTableName tableName = getTableName(tableHandle); return getTableMetadata(session, tableName); } private ConnectorTableMetadata getTableMetadata(ConnectorSession session, SchemaTableName tableName) { CassandraTableHandle tableHandle = schemaProvider.getTableHandle(tableName); List<ColumnMetadata> columns = getColumnHandles(session, tableHandle).values().stream() .map(column -> ((CassandraColumnHandle) column).getColumnMetadata()) .collect(toList()); return new ConnectorTableMetadata(tableName, columns); } @Override public List<SchemaTableName> listTables(ConnectorSession session, String schemaNameOrNull) { ImmutableList.Builder<SchemaTableName> tableNames = ImmutableList.builder(); for (String schemaName : listSchemas(session, schemaNameOrNull)) { try { for (String tableName : schemaProvider.getAllTables(schemaName)) { tableNames.add(new SchemaTableName(schemaName, tableName.toLowerCase(ENGLISH))); } } catch (SchemaNotFoundException e) { // schema disappeared during listing operation } } return tableNames.build(); } private List<String> listSchemas(ConnectorSession session, String schemaNameOrNull) { if (schemaNameOrNull == null) { return listSchemaNames(session); } return ImmutableList.of(schemaNameOrNull); } @Override public Map<String, ColumnHandle> getColumnHandles(ConnectorSession session, ConnectorTableHandle tableHandle) { CassandraTable table = schemaProvider.getTable((CassandraTableHandle) tableHandle); ImmutableMap.Builder<String, ColumnHandle> columnHandles = ImmutableMap.builder(); for (CassandraColumnHandle columnHandle : table.getColumns()) { columnHandles.put(CassandraCqlUtils.cqlNameToSqlName(columnHandle.getName()).toLowerCase(ENGLISH), columnHandle); } return columnHandles.build(); } @Override public Map<SchemaTableName, List<ColumnMetadata>> listTableColumns(ConnectorSession session, SchemaTablePrefix prefix) { requireNonNull(prefix, "prefix is null"); ImmutableMap.Builder<SchemaTableName, List<ColumnMetadata>> columns = ImmutableMap.builder(); for (SchemaTableName tableName : listTables(session, prefix)) { try { columns.put(tableName, getTableMetadata(session, tableName).getColumns()); } catch (NotFoundException e) { // table disappeared during listing operation } } return columns.build(); } private List<SchemaTableName> listTables(ConnectorSession session, SchemaTablePrefix prefix) { if (prefix.getSchemaName() == null) { return listTables(session, prefix.getSchemaName()); } return ImmutableList.of(new SchemaTableName(prefix.getSchemaName(), prefix.getTableName())); } @Override public ColumnMetadata getColumnMetadata(ConnectorSession session, ConnectorTableHandle tableHandle, ColumnHandle columnHandle) { return ((CassandraColumnHandle) columnHandle).getColumnMetadata(); } @Override public List<ConnectorTableLayoutResult> getTableLayouts(ConnectorSession session, ConnectorTableHandle table, Constraint<ColumnHandle> constraint, Optional<Set<ColumnHandle>> desiredColumns) { CassandraTableHandle handle = (CassandraTableHandle) table; CassandraPartitionResult partitionResult = partitionManager.getPartitions(handle, constraint.getSummary()); List<String> clusteringKeyPredicates; TupleDomain<ColumnHandle> unenforcedConstraint; if (partitionResult.isUnpartitioned()) { clusteringKeyPredicates = ImmutableList.of(); unenforcedConstraint = partitionResult.getUnenforcedConstraint(); } else { CassandraClusteringPredicatesExtractor clusteringPredicatesExtractor = new CassandraClusteringPredicatesExtractor( schemaProvider.getTable(handle).getClusteringKeyColumns(), partitionResult.getUnenforcedConstraint()); clusteringKeyPredicates = clusteringPredicatesExtractor.getClusteringKeyPredicates(); unenforcedConstraint = clusteringPredicatesExtractor.getUnenforcedConstraints(); } ConnectorTableLayout layout = getTableLayout(session, new CassandraTableLayoutHandle( handle, partitionResult.getPartitions(), clusteringKeyPredicates)); return ImmutableList.of(new ConnectorTableLayoutResult(layout, unenforcedConstraint)); } @Override public ConnectorTableLayout getTableLayout(ConnectorSession session, ConnectorTableLayoutHandle handle) { return new ConnectorTableLayout(handle); } @Override public String toString() { return toStringHelper(this) .add("connectorId", connectorId) .toString(); } @Override public void createTable(ConnectorSession session, ConnectorTableMetadata tableMetadata) { throw new PrestoException(NOT_SUPPORTED, "CREATE TABLE not yet supported for Cassandra"); } @Override public void dropTable(ConnectorSession session, ConnectorTableHandle tableHandle) { checkArgument(tableHandle instanceof CassandraTableHandle, "tableHandle is not an instance of CassandraTableHandle"); if (!allowDropTable) { throw new PrestoException(PERMISSION_DENIED, "DROP TABLE is disabled in this Cassandra catalog"); } CassandraTableHandle cassandraTableHandle = (CassandraTableHandle) tableHandle; String schemaName = cassandraTableHandle.getSchemaName(); String tableName = cassandraTableHandle.getTableName(); cassandraSession.execute(String.format("DROP TABLE \"%s\".\"%s\"", schemaName, tableName)); schemaProvider.flushTable(cassandraTableHandle.getSchemaTableName()); } @Override public void renameTable(ConnectorSession session, ConnectorTableHandle tableHandle, SchemaTableName newTableName) { throw new PrestoException(NOT_SUPPORTED, "Renaming tables not yet supported for Cassandra"); } @Override public ConnectorOutputTableHandle beginCreateTable(ConnectorSession session, ConnectorTableMetadata tableMetadata, Optional<ConnectorNewTableLayout> layout) { ImmutableList.Builder<String> columnNames = ImmutableList.builder(); ImmutableList.Builder<Type> columnTypes = ImmutableList.builder(); ImmutableList.Builder<ExtraColumnMetadata> columnExtra = ImmutableList.builder(); columnExtra.add(new ExtraColumnMetadata("id", true)); for (ColumnMetadata column : tableMetadata.getColumns()) { columnNames.add(column.getName()); columnTypes.add(column.getType()); columnExtra.add(new ExtraColumnMetadata(column.getName(), column.isHidden())); } // get the root directory for the database SchemaTableName table = tableMetadata.getTable(); String schemaName = schemaProvider.getCaseSensitiveSchemaName(table.getSchemaName()); String tableName = table.getTableName(); List<String> columns = columnNames.build(); List<Type> types = columnTypes.build(); StringBuilder queryBuilder = new StringBuilder(String.format("CREATE TABLE \"%s\".\"%s\"(id uuid primary key", schemaName, tableName)); for (int i = 0; i < columns.size(); i++) { String name = columns.get(i); Type type = types.get(i); queryBuilder.append(", ") .append(name) .append(" ") .append(toCassandraType(type).name().toLowerCase(ENGLISH)); } queryBuilder.append(") "); // encode column ordering in the cassandra table comment field since there is no better place to store this String columnMetadata = extraColumnMetadataCodec.toJson(columnExtra.build()); queryBuilder.append("WITH comment='").append(CassandraSession.PRESTO_COMMENT_METADATA).append(" ").append(columnMetadata).append("'"); // We need to create the Cassandra table before commit because the record needs to be written to the table. cassandraSession.execute(queryBuilder.toString()); return new CassandraOutputTableHandle( connectorId, schemaName, tableName, columnNames.build(), columnTypes.build()); } @Override public Optional<ConnectorOutputMetadata> finishCreateTable(ConnectorSession session, ConnectorOutputTableHandle tableHandle, Collection<Slice> fragments) { CassandraOutputTableHandle outputTableHandle = (CassandraOutputTableHandle) tableHandle; schemaProvider.flushTable(new SchemaTableName(outputTableHandle.getSchemaName(), outputTableHandle.getTableName())); return Optional.empty(); } }