/* * Copyright (C) 2015 Noorq, Inc. * * 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.noorq.casser.core; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; import com.datastax.driver.core.ColumnMetadata; import com.datastax.driver.core.ColumnMetadata.IndexMetadata; import com.datastax.driver.core.DataType; import com.datastax.driver.core.RegularStatement; import com.datastax.driver.core.SimpleStatement; import com.datastax.driver.core.TableMetadata; import com.datastax.driver.core.UserType; import com.datastax.driver.core.schemabuilder.Alter; import com.datastax.driver.core.schemabuilder.Create; import com.datastax.driver.core.schemabuilder.Create.Options; import com.datastax.driver.core.schemabuilder.CreateType; import com.datastax.driver.core.schemabuilder.SchemaBuilder; import com.datastax.driver.core.schemabuilder.SchemaStatement; import com.noorq.casser.mapping.CasserEntity; import com.noorq.casser.mapping.CasserEntityType; import com.noorq.casser.mapping.CasserProperty; import com.noorq.casser.mapping.ColumnType; import com.noorq.casser.mapping.OrderingDirection; import com.noorq.casser.mapping.type.OptionalColumnMetadata; import com.noorq.casser.support.CasserMappingException; import com.noorq.casser.support.CqlUtil; public final class SchemaUtil { private SchemaUtil() { } public static RegularStatement use(String keyspace, boolean forceQuote) { if (forceQuote) { return new SimpleStatement("USE" + CqlUtil.forceQuote(keyspace)); } else { return new SimpleStatement("USE " + keyspace); } } public static SchemaStatement createUserType(CasserEntity entity) { if (entity.getType() != CasserEntityType.UDT) { throw new CasserMappingException("expected UDT entity " + entity); } CreateType create = SchemaBuilder.createType(entity.getName().toCql()); for (CasserProperty prop : entity.getOrderedProperties()) { ColumnType columnType = prop.getColumnType(); if (columnType == ColumnType.PARTITION_KEY || columnType == ColumnType.CLUSTERING_COLUMN) { throw new CasserMappingException("primary key columns are not supported in UserDefinedType for " + prop.getPropertyName() + " in entity " + entity); } try { prop.getDataType().addColumn(create, prop.getColumnName()); } catch(IllegalArgumentException e) { throw new CasserMappingException("invalid column name '" + prop.getColumnName() + "' in entity '" + entity.getName().getName() + "'", e); } } return create; } public static List<SchemaStatement> alterUserType(UserType userType, CasserEntity entity, boolean dropUnusedColumns) { if (entity.getType() != CasserEntityType.UDT) { throw new CasserMappingException("expected UDT entity " + entity); } List<SchemaStatement> result = new ArrayList<SchemaStatement>(); /** * TODO: In future replace SchemaBuilder.alterTable by SchemaBuilder.alterType when it will exist */ Alter alter = SchemaBuilder.alterTable(entity.getName().toCql()); final Set<String> visitedColumns = dropUnusedColumns ? new HashSet<String>() : Collections.<String> emptySet(); for (CasserProperty prop : entity.getOrderedProperties()) { String columnName = prop.getColumnName().getName(); if (dropUnusedColumns) { visitedColumns.add(columnName); } ColumnType columnType = prop.getColumnType(); if (columnType == ColumnType.PARTITION_KEY || columnType == ColumnType.CLUSTERING_COLUMN) { continue; } DataType dataType = userType.getFieldType(columnName); SchemaStatement stmt = prop.getDataType().alterColumn(alter, prop.getColumnName(), optional(columnName, dataType)); if (stmt != null) { result.add(stmt); } } if (dropUnusedColumns) { for (String field : userType.getFieldNames()) { if (!visitedColumns.contains(field)) { result.add(alter.dropColumn(field)); } } } return result; } public static SchemaStatement dropUserType(CasserEntity entity) { if (entity.getType() != CasserEntityType.UDT) { throw new CasserMappingException("expected UDT entity " + entity); } return SchemaBuilder.dropType(entity.getName().toCql()); } public static SchemaStatement createTable(CasserEntity entity) { if (entity.getType() != CasserEntityType.TABLE) { throw new CasserMappingException("expected table entity " + entity); } Create create = SchemaBuilder.createTable(entity.getName().toCql()); create.ifNotExists(); List<CasserProperty> clusteringColumns = new ArrayList<CasserProperty>(); for (CasserProperty prop : entity.getOrderedProperties()) { ColumnType columnType = prop.getColumnType(); if (columnType == ColumnType.CLUSTERING_COLUMN) { clusteringColumns.add(prop); } prop.getDataType().addColumn(create, prop.getColumnName()); } if (!clusteringColumns.isEmpty()) { Options options = create.withOptions(); clusteringColumns.forEach(p -> options.clusteringOrder(p.getColumnName().toCql(), mapDirection(p.getOrdering()))); } return create; } public static List<SchemaStatement> alterTable(TableMetadata tmd, CasserEntity entity, boolean dropUnusedColumns) { if (entity.getType() != CasserEntityType.TABLE) { throw new CasserMappingException("expected table entity " + entity); } List<SchemaStatement> result = new ArrayList<SchemaStatement>(); Alter alter = SchemaBuilder.alterTable(entity.getName().toCql()); final Set<String> visitedColumns = dropUnusedColumns ? new HashSet<String>() : Collections.<String> emptySet(); for (CasserProperty prop : entity.getOrderedProperties()) { String columnName = prop.getColumnName().getName(); if (dropUnusedColumns) { visitedColumns.add(columnName); } ColumnType columnType = prop.getColumnType(); if (columnType == ColumnType.PARTITION_KEY || columnType == ColumnType.CLUSTERING_COLUMN) { continue; } ColumnMetadata columnMetadata = tmd.getColumn(columnName); SchemaStatement stmt = prop.getDataType().alterColumn(alter, prop.getColumnName(), optional(columnMetadata)); if (stmt != null) { result.add(stmt); } } if (dropUnusedColumns) { for (ColumnMetadata cm : tmd.getColumns()) { if (!visitedColumns.contains(cm.getName())) { result.add(alter.dropColumn(cm.getName())); } } } return result; } public static SchemaStatement dropTable(CasserEntity entity) { if (entity.getType() != CasserEntityType.TABLE) { throw new CasserMappingException("expected table entity " + entity); } return SchemaBuilder.dropTable(entity.getName().toCql()).ifExists(); } public static SchemaStatement createIndex(CasserProperty prop) { return SchemaBuilder.createIndex(prop.getIndexName().get().toCql()) .ifNotExists() .onTable(prop.getEntity().getName().toCql()) .andColumn(prop.getColumnName().toCql()); } public static List<SchemaStatement> createIndexes(CasserEntity entity) { return entity.getOrderedProperties().stream() .filter(p -> p.getIndexName().isPresent()) .map(p -> SchemaUtil.createIndex(p)) .collect(Collectors.toList()); } public static List<SchemaStatement> alterIndexes(TableMetadata tmd, CasserEntity entity, boolean dropUnusedIndexes) { List<SchemaStatement> list = new ArrayList<SchemaStatement>(); final Set<String> visitedColumns = dropUnusedIndexes ? new HashSet<String>() : Collections.<String> emptySet(); entity .getOrderedProperties() .stream() .filter(p -> p.getIndexName().isPresent()) .forEach(p -> { String columnName = p.getColumnName().getName(); if (dropUnusedIndexes) { visitedColumns.add(columnName); } ColumnMetadata cm = tmd.getColumn(columnName); if (cm != null) { IndexMetadata im = cm.getIndex(); if (im == null) { list.add(createIndex(p)); } } else { list.add(createIndex(p)); } }); if (dropUnusedIndexes) { tmd .getColumns() .stream() .filter(c -> c.getIndex() != null && !visitedColumns.contains(c.getName())) .forEach(c -> { list.add(SchemaBuilder.dropIndex(c.getIndex().getName()).ifExists()); }); } return list; } public static SchemaStatement dropIndex(CasserProperty prop) { return SchemaBuilder.dropIndex(prop.getIndexName().get().toCql()).ifExists(); } private static SchemaBuilder.Direction mapDirection(OrderingDirection o) { switch(o) { case ASC: return SchemaBuilder.Direction.ASC; case DESC: return SchemaBuilder.Direction.DESC; } throw new CasserMappingException("unknown ordering " + o); } public static void throwNoMapping(CasserProperty prop) { throw new CasserMappingException( "only primitive types and Set,List,Map collections and UserDefinedTypes are allowed, unknown type for property '" + prop.getPropertyName() + "' type is '" + prop.getJavaType() + "' in the entity " + prop.getEntity()); } private static OptionalColumnMetadata optional(final ColumnMetadata columnMetadata) { if (columnMetadata != null) { return new OptionalColumnMetadata() { @Override public String getName() { return columnMetadata.getName(); } @Override public DataType getType() { return columnMetadata.getType(); } }; } return null; } private static OptionalColumnMetadata optional(final String name, final DataType dataType) { if (dataType != null) { return new OptionalColumnMetadata() { @Override public String getName() { return name; } @Override public DataType getType() { return dataType; } }; } return null; } }