/** * Copyright (C) 2009-2013 FoundationDB, LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.foundationdb.server.service.dxl; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.Callable; import java.util.LinkedList; import java.util.Deque; import com.foundationdb.ais.AISCloner; import com.foundationdb.ais.model.AbstractVisitor; import com.foundationdb.ais.model.AkibanInformationSchema; import com.foundationdb.ais.model.Column; import com.foundationdb.ais.model.ColumnName; import com.foundationdb.ais.model.Columnar; import com.foundationdb.ais.model.ForeignKey; import com.foundationdb.ais.model.Group; import com.foundationdb.ais.model.GroupIndex; import com.foundationdb.ais.model.HasStorage; import com.foundationdb.ais.model.Index; import com.foundationdb.ais.model.Index.IndexType; import com.foundationdb.ais.model.IndexColumn; import com.foundationdb.ais.model.Join; import com.foundationdb.ais.model.Routine; import com.foundationdb.ais.model.Schema; import com.foundationdb.ais.model.Sequence; import com.foundationdb.ais.model.SQLJJar; import com.foundationdb.ais.model.Table; import com.foundationdb.ais.model.TableIndex; import com.foundationdb.ais.model.TableName; import com.foundationdb.ais.model.View; import com.foundationdb.ais.protobuf.ProtobufWriter.TableSelector; import com.foundationdb.ais.util.ChangedTableDescription; import com.foundationdb.ais.util.TableChange; import com.foundationdb.ais.util.TableChangeValidatorState; import com.foundationdb.ais.util.TableChangeValidator; import com.foundationdb.qp.operator.QueryContext; import com.foundationdb.qp.operator.StoreAdapter; import com.foundationdb.server.api.DDLFunctions; import com.foundationdb.server.api.DMLFunctions; import com.foundationdb.server.error.AlterMadeNoChangeException; import com.foundationdb.server.error.ConcurrentViolationException; import com.foundationdb.server.error.DropSequenceNotAllowedException; import com.foundationdb.server.error.ForeignConstraintDDLException; import com.foundationdb.server.error.ForeignKeyPreventsDropTableException; import com.foundationdb.server.error.NoSuchGroupException; import com.foundationdb.server.error.NoSuchIndexException; import com.foundationdb.server.error.NoSuchSequenceException; import com.foundationdb.server.error.NoSuchTableException; import com.foundationdb.server.error.NoSuchTableIdException; import com.foundationdb.server.error.ProtectedIndexException; import com.foundationdb.server.error.ReferencedSQLJJarException; import com.foundationdb.server.error.UnsupportedDropException; import com.foundationdb.server.error.ViewReferencesExist; import com.foundationdb.server.error.SQLParserInternalException; import com.foundationdb.server.error.UnsupportedCreateSelectException; import com.foundationdb.server.service.config.ConfigurationService; import com.foundationdb.server.service.listener.ListenerService; import com.foundationdb.server.service.listener.TableListener; import com.foundationdb.server.service.session.Session; import com.foundationdb.server.service.transaction.TransactionService; import com.foundationdb.server.store.ChangeSetHelper; import com.foundationdb.server.store.OnlineHelper; import com.foundationdb.server.store.SchemaManager; import com.foundationdb.server.store.Store; import com.foundationdb.server.store.TableChanges.ChangeSet; import com.foundationdb.server.store.TableChanges; import com.foundationdb.server.store.format.StorageFormatRegistry; import com.foundationdb.server.store.statistics.IndexStatisticsService; import com.foundationdb.server.types.aksql.aktypes.AkBlob; import com.foundationdb.server.types.common.types.TypesTranslator; import com.foundationdb.server.types.service.TypesRegistry; import com.foundationdb.sql.StandardException; import com.foundationdb.sql.compiler.BooleanNormalizer; import com.foundationdb.sql.optimizer.AISBinder; import com.foundationdb.sql.optimizer.CreateAsCompiler; import com.foundationdb.sql.optimizer.SubqueryFlattener; import com.foundationdb.sql.optimizer.plan.*; import com.foundationdb.sql.optimizer.rule.ASTStatementLoader; import com.foundationdb.sql.optimizer.rule.PlanContext; import com.foundationdb.sql.parser.DMLStatementNode; import com.foundationdb.sql.parser.SQLParser; import com.foundationdb.sql.parser.StatementNode; import com.foundationdb.sql.server.ServerSession; import com.google.common.collect.HashMultimap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static com.foundationdb.ais.util.TableChangeValidator.ChangeLevel; public class BasicDDLFunctions implements DDLFunctions { private final static Logger logger = LoggerFactory.getLogger(BasicDDLFunctions.class); private final SchemaManager schemaManager; private final Store store; private final IndexStatisticsService indexStatisticsService; private final TransactionService txnService; private final ListenerService listenerService; private OnlineDDLMonitor onlineDDLMonitor; @Override public void createTable(Session session, Table table) { schemaManager.checkAllowedIndexes(table.getIndexesIncludingInternal()); TableName tableName = schemaManager().createTableDefinition(session, table); Table newTable = getAIS(session).getTable(tableName); for(TableListener listener : listenerService.getTableListeners()) { listener.onCreate(session, newTable); } } private List<TableName> getTableNames(Session session, ServerSession server, String queryExpression, Table table ){ AkibanInformationSchema ais = schemaManager().getAis(session); SQLParser parser = server.getParser(); StatementNode stmt; try { stmt = parser.parseStatement(queryExpression); } catch (StandardException e) { throw new SQLParserInternalException(e); } StoreAdapter adapter = store().createAdapter(session); CreateAsCompiler compiler = new CreateAsCompiler(server, adapter, false, ais); PlanContext plan = new PlanContext(compiler); ASTStatementLoader astStatementLoader = new ASTStatementLoader(); AISBinder binder = new AISBinder(ais, table.getName().getSchemaName()); try { binder.bind(stmt); BooleanNormalizer booleanNormalizer = new BooleanNormalizer(parser); stmt = booleanNormalizer.normalize(stmt); SubqueryFlattener subqueryFlattener = new SubqueryFlattener(parser); stmt = subqueryFlattener.flatten((DMLStatementNode)stmt); } catch (StandardException ex) { throw new SQLParserInternalException(ex); } plan.setPlan(new AST((DMLStatementNode)stmt, null)); astStatementLoader.apply(plan); List<TableName> tableNames = new ArrayList<>(); Deque<PlanNode> nodeQueue = new LinkedList<>(); nodeQueue.add(plan.getPlan()); while(!nodeQueue.isEmpty()){ PlanNode node = nodeQueue.poll(); if(node instanceof BasePlanWithInput){ nodeQueue.add(((BasePlanWithInput)node).getInput()); } if(node instanceof TableSource) { tableNames.add(((TableSource) node).getTable().getTable().getName()); }else if(node instanceof Select && !((Select)node).getConditions().isEmpty()) { ConditionList conditionList = ((Select) node).getConditions(); for (ConditionExpression conditionExpression : conditionList.subList(0, conditionList.size())) { nodeQueue.add(((AnyCondition) conditionExpression).getSubquery()); } }else if( node instanceof JoinNode) { nodeQueue.add(((JoinNode)node).getLeft()); nodeQueue.add(((JoinNode)node).getRight()); } } return tableNames; } @Override public void createTable(final Session session, final Table table, final String queryExpression, QueryContext context, final ServerSession server){ if(queryExpression == null || queryExpression.isEmpty()){ createTable(session, table); return; } logger.debug("creating table {}", table); schemaManager.checkAllowedIndexes(table.getIndexesIncludingInternal()); txnService.commitTransaction(session); try { onlineAt(OnlineDDLMonitor.Stage.PRE_METADATA); txnService.run(session, new Runnable() { @Override public void run() { schemaManager().startOnline(session); TableName tableName = schemaManager().createTableDefinition(session, table); AkibanInformationSchema onlineAIS = schemaManager().getOnlineAIS(session); int onlineTableID = onlineAIS.getTable(table.getName()).getTableId(); List<TableName> tableNames = getTableNames(session, server, queryExpression, table); if(tableNames.size() > 1) throw new UnsupportedCreateSelectException(); for( TableName name : tableNames){ ChangeSet fromChangeSet = buildChangeSet(onlineAIS.getTable(name), queryExpression, onlineTableID); schemaManager().addOnlineChangeSet(session, fromChangeSet); } ChangeSet toChangeSet = buildChangeSet(onlineAIS.getTable(tableName), queryExpression, onlineTableID); schemaManager().addOnlineChangeSet(session, toChangeSet); } }); onlineAt(OnlineDDLMonitor.Stage.POST_METADATA); final boolean[] success = {false}; try { onlineAt(OnlineDDLMonitor.Stage.PRE_TRANSFORM); store().getOnlineHelper().createAsSelect(session, context, server, queryExpression, table.getName()); onlineAt(OnlineDDLMonitor.Stage.POST_TRANSFORM); txnService.run(session, new Runnable() { @Override public void run() { AkibanInformationSchema onlineAIS = schemaManager().getOnlineAIS(session); final Table onlineTable = onlineAIS.getTable(table.getName()); for (TableListener listener : listenerService.getTableListeners()) { listener.onCreate(session, onlineTable); } } }); success[0] = true; } finally { onlineAt(OnlineDDLMonitor.Stage.PRE_FINAL); txnService.run(session, new Runnable() { @Override public void run() { if (success[0]) { finishOnlineChange(session); } else { discardOnlineChange(session); } } }); onlineAt(OnlineDDLMonitor.Stage.POST_FINAL); } }finally { txnService.beginTransaction(session); } } @Override public void renameTable(Session session, TableName currentName, TableName newName) { schemaManager().renameTable(session, currentName, newName); } @Override public void dropTable(Session session, TableName tableName) { txnService.beginTransaction(session); try { dropTableInternal(session, tableName); txnService.commitTransaction(session); } finally { txnService.rollbackTransactionIfOpen(session); } } private void dropTableInternal(Session session, TableName tableName) { logger.trace("dropping table {}", tableName); Table table = getAIS(session).getTable(tableName); if(table == null) { return; } // May only drop leaf tables through DDL interface if(!table.getChildJoins().isEmpty()) { throw new UnsupportedDropException(table.getName()); } DMLFunctions dml = new BasicDMLFunctions(schemaManager(), store(), listenerService); if(table.isRoot() && !containsBlob(table)) { // Root table and no child tables, can delete all associated trees store().removeTrees(session, table); } else { dml.truncateTable(session, table.getTableId(), false); store().deleteIndexes(session, table.getIndexesIncludingInternal()); store().deleteIndexes(session, table.getGroupIndexes()); if (table.getIdentityColumn() != null) { Collection<Sequence> sequences = Collections.singleton(table.getIdentityColumn().getIdentityGenerator()); store().deleteSequences(session, sequences); } } for(TableListener listener : listenerService.getTableListeners()) { listener.onDrop(session, table); } schemaManager().dropTableDefinition(session, tableName.getSchemaName(), tableName.getTableName(), SchemaManager.DropBehavior.RESTRICT); } public static boolean containsBlob(Table table) { for (Column column : table.getColumns()) { if (AkBlob.isBlob(column.getType().typeClass())) return true; } return false; } @Override public ChangeLevel alterTable(final Session session, final TableName tableName, final Table newDefinition, final List<TableChange> columnChanges, final List<TableChange> tableIndexChanges, final QueryContext context) { schemaManager.checkAllowedIndexes(newDefinition.getIndexesIncludingInternal()); onlineAt(OnlineDDLMonitor.Stage.PRE_METADATA); final AISValidatorPair pair = txnService.run(session, new Callable<AISValidatorPair>() { @Override public AISValidatorPair call() { AkibanInformationSchema origAIS = getAIS(session); Table origTable = origAIS.getTable(tableName); schemaManager().startOnline(session); TableChangeValidator validator = alterTableDefinitions( session, origTable, newDefinition, columnChanges, tableIndexChanges ); List<ChangeSet> changeSets = buildChangeSets( origAIS, schemaManager().getOnlineAIS(session), origTable.getTableId(), validator ); for(ChangeSet cs : changeSets) { schemaManager().addOnlineChangeSet(session, cs); } return new AISValidatorPair(origAIS, validator); } }); onlineAt(OnlineDDLMonitor.Stage.POST_METADATA); final String errorMsg; final boolean[] success = { false }; try { onlineAt(OnlineDDLMonitor.Stage.PRE_TRANSFORM); alterTablePerform(session, tableName, pair.validator.getFinalChangeLevel(), context); onlineAt(OnlineDDLMonitor.Stage.POST_TRANSFORM); success[0] = true; } finally { onlineAt(OnlineDDLMonitor.Stage.PRE_FINAL); errorMsg = txnService.run(session, new Callable<String>() { @Override public String call() { String error = schemaManager().getOnlineDMLError(session); if(success[0] && (error == null)) { finishOnlineChange(session); } else { discardOnlineChange(session); } return error; } }); onlineAt(OnlineDDLMonitor.Stage.POST_FINAL); } if(errorMsg != null) { throw new ConcurrentViolationException(errorMsg); } // Clear old storage after it is completely unused txnService.run(session, new Runnable() { @Override public void run() { Table origTable = pair.ais.getTable(tableName); Table newTable = getTable(session, origTable.getTableId()); alterTableRemoveOldStorage(session, origTable, newTable, pair.validator); } }); return pair.validator.getFinalChangeLevel(); } @Override public void alterSequence(Session session, TableName sequenceName, Sequence newDefinition) { logger.trace("altering sequence {}", sequenceName); AkibanInformationSchema ais = getAIS(session); Sequence oldSeq = ais.getSequence(sequenceName); if(oldSeq == null) { throw new NoSuchSequenceException(sequenceName); } schemaManager().alterSequence(session, sequenceName, newDefinition); // Remove old storage store().deleteSequences(session, Collections.singleton(oldSeq)); } @Override public void dropSchema(Session session, String schemaName) { logger.trace("dropping schema {}", schemaName); txnService.beginTransaction(session); try { dropSchemaInternal(session, schemaName); txnService.commitTransaction(session); } finally { txnService.rollbackTransactionIfOpen(session); } } @Override public void dropNonSystemSchemas(Session session) { logger.trace("dropping non system schemas"); txnService.beginTransaction(session); try { Collection<Schema> schemas = new ArrayList<>(getAIS(session).getSchemas().values()); for (Schema schema : schemas) { if (!TableName.inSystemSchema(schema.getName())) { for (Table table : schema.getTables().values()) { for (TableListener listener : listenerService.getTableListeners()) { listener.onDrop(session, table); } } } } schemaManager().dropNonSystemSchemas(session); store().dropNonSystemSchemas(session, schemas); store().dropAllLobs(session); // drop full text indexes txnService.commitTransaction(session); } finally { txnService.rollbackTransactionIfOpen(session); } } private void dropSchemaInternal(Session session, String schemaName) { final com.foundationdb.ais.model.Schema schema = getAIS(session).getSchema(schemaName); if (schema == null) return; // NOT throw new NoSuchSchemaException(schemaName); adapter does it. List<View> viewsToDrop = new ArrayList<>(); Set<View> seen = new HashSet<>(); for (View view : schema.getViews().values()) { addView(view, viewsToDrop, seen, schema, schemaName); } // Find all groups and tables in the schema List<Table> explicitlyDroppedTables = new ArrayList<>(); List<Table> implicitlyDroppedTables = new ArrayList<>(); for(Table table : schema.getTables().values()) { // Cannot drop entire group if parent is not in the same schema final Join parentJoin = table.getParentJoin(); if (containsBlob(table)) { explicitlyDroppedTables.add(table); } else if(parentJoin != null) { final Table parentTable = parentJoin.getParent(); if (!parentTable.getName().getSchemaName().equals(schemaName)) { explicitlyDroppedTables.add(table); } else { implicitlyDroppedTables.add(table); } } else { implicitlyDroppedTables.add(table); } // All children must be in the same schema for(Join childJoin : table.getChildJoins()) { final TableName childName = childJoin.getChild().getName(); if(!childName.getSchemaName().equals(schemaName)) { throw new ForeignConstraintDDLException(table.getName(), childName); } } // All referencing foreign keys must be in the same schema for(ForeignKey foreignKey : table.getReferencedForeignKeys()) { final TableName referencingName = foreignKey.getReferencingTable().getName(); if(!referencingName.getSchemaName().equals(schemaName)) { throw new ForeignKeyPreventsDropTableException(table.getName(), foreignKey.getConstraintName().getTableName(), referencingName); } } } for (SQLJJar jar : schema.getSQLJJars().values()) { for (Routine routine : jar.getRoutines()) { if (!routine.getName().getSchemaName().equals(schemaName)) { throw new ReferencedSQLJJarException(jar); } } } for (Table table : explicitlyDroppedTables) { dropTableAndChildren(session, table); } for (Table table : implicitlyDroppedTables) { for(TableListener listener : listenerService.getTableListeners()) { listener.onDrop(session, table); } } schemaManager().dropSchema(session, schemaName); store().dropSchema(session, schema); } private void dropTableAndChildren(Session session, Table table) { for (Join childJoin : table.getChildJoins()) { dropTableAndChildren(session, childJoin.getChild()); } dropTableInternal(session, table.getName()); } private void addView(View view, Collection<View> into, Collection<View> seen, com.foundationdb.ais.model.Schema schema, String schemaName) { if (seen.add(view)) { for (TableName reference : view.getTableReferences()) { if (!reference.getSchemaName().equals(schemaName)) { throw new ViewReferencesExist(schemaName, view.getName().getTableName(), reference.getSchemaName(), reference.getTableName()); } // If reference is to another view, it must come first. View refView = schema.getView(reference.getTableName()); if (refView != null) { addView(view, into, seen, schema, schemaName); } } into.add(view); } } @Override public void dropGroup(Session session, TableName groupName) { logger.trace("dropping group {}", groupName); txnService.beginTransaction(session); try { dropGroupInternal(session, groupName); txnService.commitTransaction(session); } finally { txnService.rollbackTransactionIfOpen(session); } } private void dropGroupInternal(final Session session, TableName groupName) { final Group group = getAIS(session).getGroup(groupName); if(group == null) { return; } store().dropGroup(session, group); group.visit(new AbstractVisitor() { @Override public void visit(Table table) { for(TableListener listener : listenerService.getTableListeners()) { listener.onDrop(session, table); } } }); Table root = group.getRoot(); schemaManager().dropTableDefinition(session, root.getName().getSchemaName(), root.getName().getTableName(), SchemaManager.DropBehavior.CASCADE); } @Override public AkibanInformationSchema getAIS(final Session session) { logger.trace("getting AIS"); return schemaManager().getAis(session); } @Override public TypesRegistry getTypesRegistry() { return schemaManager().getTypesRegistry(); } @Override public TypesTranslator getTypesTranslator() { return schemaManager().getTypesTranslator(); } @Override public StorageFormatRegistry getStorageFormatRegistry() { return schemaManager().getStorageFormatRegistry(); } @Override public AISCloner getAISCloner() { return schemaManager().getAISCloner(); } @Override public int getTableId(Session session, TableName tableName) throws NoSuchTableException { logger.trace("getting table ID for {}", tableName); Table table = getAIS(session).getTable(tableName); if (table == null) { throw new NoSuchTableException(tableName); } return table.getTableId(); } @Override public Table getTable(Session session, int tableId) throws NoSuchTableIdException { logger.trace("getting AIS Table for {}", tableId); Table table = getAIS(session).getTable(tableId); if(table == null) { throw new NoSuchTableIdException(tableId); } return table; } @Override public Table getTable(Session session, TableName tableName) throws NoSuchTableException { logger.trace("getting AIS Table for {}", tableName); AkibanInformationSchema ais = getAIS(session); Table table = ais.getTable(tableName); if (table == null) { throw new NoSuchTableException(tableName); } return table; } @Override public TableName getTableName(Session session, int tableId) throws NoSuchTableException { logger.trace("getting table name for {}", tableId); return getTable(session, tableId).getName(); } @Override public int getGenerationAsInt(Session session) { long full = getGeneration(session); return (int)full ^ (int)(full >>> 32); } @Override public long getGeneration(Session session) { return getAIS(session).getGeneration(); } @Override public long getOldestActiveGeneration() { return schemaManager().getOldestActiveAISGeneration(); } @Override public Set<Long> getActiveGenerations() { return schemaManager().getActiveAISGenerations(); } @Override public void createIndexes(final Session session, final Collection<? extends Index> indexesToAdd) { logger.debug("creating indexes {}", indexesToAdd); if (indexesToAdd.isEmpty()) { return; } schemaManager.checkAllowedIndexes(indexesToAdd); onlineAt(OnlineDDLMonitor.Stage.PRE_METADATA); txnService.run(session, new Runnable() { @Override public void run() { schemaManager().startOnline(session); schemaManager().createIndexes(session, indexesToAdd, false); AkibanInformationSchema onlineAIS = schemaManager().getOnlineAIS(session); List<ChangeSet> changeSets = buildChangeSets(onlineAIS, indexesToAdd); for(ChangeSet cs : changeSets) { schemaManager().addOnlineChangeSet(session, cs); } } }); onlineAt(OnlineDDLMonitor.Stage.POST_METADATA); final String errorMsg; final boolean[] success = { false }; try { onlineAt(OnlineDDLMonitor.Stage.PRE_TRANSFORM); store().getOnlineHelper().buildIndexes(session, null); onlineAt(OnlineDDLMonitor.Stage.POST_TRANSFORM); txnService.run(session, new Runnable() { @Override public void run() { Collection<ChangeSet> changeSets = schemaManager().getOnlineChangeSets(session); AkibanInformationSchema onlineAIS = schemaManager().getOnlineAIS(session); Collection<Index> newIndexes = OnlineHelper.findIndexesToBuild(changeSets, onlineAIS); for(TableListener listener : listenerService.getTableListeners()) { listener.onCreateIndex(session, newIndexes); } } }); success[0] = true; } finally { onlineAt(OnlineDDLMonitor.Stage.PRE_FINAL); errorMsg = txnService.run(session, new Callable<String>() { @Override public String call() { String error = schemaManager().getOnlineDMLError(session); if(success[0] && (error == null)) { finishOnlineChange(session); } else { discardOnlineChange(session); } return error; } }); onlineAt(OnlineDDLMonitor.Stage.POST_FINAL); } if(errorMsg != null) { throw new ConcurrentViolationException(errorMsg); } } @Override public void dropTableIndexes(Session session, TableName tableName, Collection<String> indexNamesToDrop) { logger.trace("dropping table indexes {} {}", tableName, indexNamesToDrop); if(indexNamesToDrop.isEmpty()) { return; } txnService.beginTransaction(session); try { final Table table = getTable(session, tableName); Collection<Index> tableIndexes = new HashSet<>(); Collection<Index> allIndexes = tableIndexes; for(String indexName : indexNamesToDrop) { Index index = table.getIndex(indexName); if(index != null) { tableIndexes.add(index); } else if ((index = table.getFullTextIndex(indexName)) != null) { if (allIndexes == tableIndexes) { allIndexes = new HashSet<>(allIndexes); } } else { throw new NoSuchIndexException(indexName); } // PRIMARY is special (affects grouping). FOREIGN KEY referenced handled in validations. if(index.isPrimaryKey()) { throw new ProtectedIndexException(indexName, table.getName()); } if (allIndexes != tableIndexes) { allIndexes.add(index); } } schemaManager().dropIndexes(session, allIndexes); store().deleteIndexes(session, tableIndexes); for(TableListener listener : listenerService.getTableListeners()) { listener.onDropIndex(session, allIndexes); } txnService.commitTransaction(session); } finally { txnService.rollbackTransactionIfOpen(session); } } @Override public void dropGroupIndexes(Session session, TableName groupName, Collection<String> indexNamesToDrop) { logger.trace("dropping group indexes {} {}", groupName, indexNamesToDrop); if(indexNamesToDrop.isEmpty()) { return; } txnService.beginTransaction(session); try { final Group group = getAIS(session).getGroup(groupName); if (group == null) { throw new NoSuchGroupException(groupName); } Collection<Index> indexes = new HashSet<>(); for(String indexName : indexNamesToDrop) { final Index index = group.getIndex(indexName); if(index == null) { throw new NoSuchIndexException(indexName); } indexes.add(index); } schemaManager().dropIndexes(session, indexes); store().deleteIndexes(session, indexes); for(TableListener listener : listenerService.getTableListeners()) { listener.onDropIndex(session, indexes); } txnService.commitTransaction(session); } finally { txnService.rollbackTransactionIfOpen(session); } } @Override public void updateTableStatistics(Session session, TableName tableName, Collection<String> indexesToUpdate) { final Table table = getTable(session, tableName); Collection<Index> indexes = new HashSet<>(); if (indexesToUpdate == null) { indexes.addAll(table.getIndexes()); for (Index index : table.getGroup().getIndexes()) { if (table == index.leafMostTable()) indexes.add(index); } } else { for (String indexName : indexesToUpdate) { Index index = table.getIndex(indexName); if (index == null) { index = table.getGroup().getIndex(indexName); if (index == null) throw new NoSuchIndexException(indexName); } indexes.add(index); } } indexStatisticsService.updateIndexStatistics(session, indexes); } @Override public void createView(Session session, View view) { schemaManager().createView(session, view); } @Override public void dropView(Session session, TableName viewName) { schemaManager().dropView(session, viewName); } @Override public void createRoutine(Session session, Routine routine, boolean replaceExisting) { schemaManager().createRoutine(session, routine, replaceExisting); } @Override public void dropRoutine(Session session, TableName routineName) { schemaManager().dropRoutine(session, routineName); } @Override public void createSQLJJar(Session session, SQLJJar sqljJar) { schemaManager().createSQLJJar(session, sqljJar); } @Override public void replaceSQLJJar(Session session, SQLJJar sqljJar) { schemaManager().replaceSQLJJar(session, sqljJar); } @Override public void dropSQLJJar(Session session, TableName jarName) { schemaManager().dropSQLJJar(session, jarName); } @Override public synchronized void setOnlineDDLMonitor(OnlineDDLMonitor onlineDDLMonitor) { assert (this.onlineDDLMonitor == null || onlineDDLMonitor == null); this.onlineDDLMonitor = onlineDDLMonitor; } public void createSequence(Session session, Sequence sequence) { schemaManager().createSequence(session, sequence); } public void dropSequence(Session session, TableName sequenceName) { final Sequence sequence = getAIS(session).getSequence(sequenceName); if (sequence == null) { throw new NoSuchSequenceException (sequenceName); } for (Table table : getAIS(session).getTables().values()) { if (table.getIdentityColumn() != null && table.getIdentityColumn().getIdentityGenerator().equals(sequence)) { throw new DropSequenceNotAllowedException(sequence.getSequenceName().getTableName(), table.getName()); } } store().deleteSequences(session, Collections.singleton(sequence)); schemaManager().dropSequence(session, sequence); } // // Internal // BasicDDLFunctions(SchemaManager schemaManager, Store store, IndexStatisticsService indexStatisticsService, TransactionService txnService, ListenerService listenerService, ConfigurationService configService) { this.schemaManager = schemaManager; this.store = store; this.indexStatisticsService = indexStatisticsService; this.txnService = txnService; this.listenerService = listenerService; } private TableChangeValidator alterTableDefinitions(Session session, Table origTable, Table newDefinition, List<TableChange> columnChanges, List<TableChange> tableIndexChanges) { // Any GroupIndex changes are entirely derived, ignore any that happen to be passed. if(newDefinition.getGroup() != null) { newDefinition.getGroup().removeIndexes(newDefinition.getGroup().getIndexes()); } TableChangeValidator v = new TableChangeValidator(origTable, newDefinition, columnChanges, tableIndexChanges); v.compare(); TableChangeValidatorState changeState = v.getState(); dropGroupIndexDefinitions(session, origTable, changeState.droppedGI); Table newTable = schemaManager().getOnlineAIS(session).getTable(origTable.getTableId()); dropGroupIndexDefinitions(session, newTable, changeState.affectedGI.keySet()); schemaManager().alterTableDefinitions(session, changeState.descriptions); newTable = schemaManager().getOnlineAIS(session).getTable(origTable.getTableId()); recreateGroupIndexes(session, changeState, origTable, newTable); return v; } private void alterTablePerform(Session session, TableName tableName, ChangeLevel level, QueryContext context) { switch(level) { case NONE: AlterMadeNoChangeException e = new AlterMadeNoChangeException(tableName); logger.warn(e.getMessage()); if(context != null) { context.warnClient(e); } break; case METADATA: // None break; case METADATA_CONSTRAINT: store().getOnlineHelper().checkTableConstraints(session, context); break; case INDEX: case INDEX_CONSTRAINT: store().getOnlineHelper().buildIndexes(session, context); break; case TABLE: case GROUP: store().getOnlineHelper().alterTable(session, context); break; default: throw new IllegalStateException(level.toString()); } } private void discardOnlineChange(Session session) { Collection<ChangeSet> changeSets = schemaManager().getOnlineChangeSets(session); store().discardOnlineChange(session, changeSets); schemaManager().discardOnline(session); } private void finishOnlineChange(Session session) { Collection<ChangeSet> changeSets = schemaManager().getOnlineChangeSets(session); schemaManager().finishOnline(session); store().finishOnlineChange(session, changeSets); } private void alterTableRemoveOldStorage(Session session, Table origTable, Table newTable, TableChangeValidator validator) { AkibanInformationSchema origAIS = origTable.getAIS(); TableChangeValidatorState changeState = validator.getState(); ChangeLevel changeLevel = validator.getFinalChangeLevel(); List<Sequence> sequencesToDrop = new ArrayList<>(); for(ChangedTableDescription desc : changeState.descriptions) { for(TableName name : desc.getDroppedSequences()) { sequencesToDrop.add(origAIS.getSequence(name)); } } List<Index> indexesToDrop = new ArrayList<>(); Group group = origTable.getGroup(); for(String name : changeState.droppedGI) { indexesToDrop.add(group.getIndex(name)); } List<HasStorage> storageToRemove = new ArrayList<>(); for(TableChange ic : validator.getState().tableIndexChanges) { switch(ic.getChangeType()) { case MODIFY: case DROP: Index index = origTable.getIndexIncludingInternal(ic.getOldName()); storageToRemove.add(index); break; } } // Old group storage if(changeLevel == ChangeLevel.TABLE || changeLevel == ChangeLevel.GROUP) { storageToRemove.add(origTable.getGroup()); } // New parent's old group storage // but only if the storage location is different - metadata changes may not move the group data Table newParent = newTable.getParentTable(); if(newParent != null) { Table newParentOldTable = origAIS.getTable(newParent.getTableId()); if (!newParent.getGroup().getStorageDescription().getUniqueKey().equals( newParentOldTable.getGroup().getStorageDescription().getUniqueKey())) { storageToRemove.add(newParentOldTable.getGroup()); } } store().deleteIndexes(session, indexesToDrop); store().deleteSequences(session, sequencesToDrop); store().removeTrees(session, storageToRemove); } private void dropGroupIndexDefinitions(Session session, Table table, Collection<String> names) { if(names.isEmpty()) { return; } List<GroupIndex> groupIndexes = new ArrayList<>(); for(String n : names) { GroupIndex index = table.getGroup().getIndex(n); assert (index != null) : n; groupIndexes.add(index); } schemaManager().dropIndexes(session, groupIndexes); } private void recreateGroupIndexes(Session session, TableChangeValidatorState changeState, Table origTable, final Table newTable) { if(changeState.affectedGI.isEmpty()) { return; } AkibanInformationSchema tempAIS = getAISCloner().clone( newTable.getAIS(), new TableSelector() { @Override public boolean isSelected(Columnar columnar) { if(columnar.isTable()) { if(((Table)columnar).getGroup() == newTable.getGroup()) { return true; } } return false; } @Override public boolean isSelected(ForeignKey foreignKey) { return false; } } ); List<Index> indexesToBuild = new ArrayList<>(); Group origGroup = origTable.getGroup(); Group tempGroup = tempAIS.getGroup(newTable.getGroup().getName()); for(Entry<String, List<ColumnName>> entry : changeState.affectedGI.entrySet()) { GroupIndex origIndex = origGroup.getIndex(entry.getKey()); GroupIndex tempIndex = GroupIndex.create(tempAIS, tempGroup, origIndex); int pos = 0; for(ColumnName cn : entry.getValue()) { Table tempTable = tempAIS.getTable(cn.getTableName()); Column tempColumn = tempTable.getColumn(cn.getName()); IndexColumn.create(tempIndex, tempColumn, pos++, true, null); } if(!changeState.dataAffectedGI.containsKey(entry.getKey())) { // TODO: Maybe need a way to say copy without the tree name part? tempIndex.copyStorageDescription(origIndex); } indexesToBuild.add(tempIndex); } schemaManager().createIndexes(session, indexesToBuild, true); } // // Internal static // private static ChangedTableDescription findByID(List<ChangedTableDescription> descriptions, int tableID) { for(ChangedTableDescription d : descriptions) { if(d.getTableID() == tableID) { return d; } } return null; } /** Find indexes from the old table that 1) were not preserved and 2) are present in the new table. */ private static Collection<TableIndex> findTableIndexesToBuild(ChangedTableDescription desc, Table oldTable, Table newTable) { List<TableIndex> indexes = new ArrayList<>(); for(TableIndex index : oldTable.getIndexesIncludingInternal()) { String oldName = index.getIndexName().getName(); String preserveName = desc.getPreserveIndexes().get(oldName); if(preserveName == null) { TableIndex newIndex = newTable.getIndexIncludingInternal(oldName); if(newIndex != null) { indexes.add(newIndex); } } } return indexes; } /** ChangeSets for create table as */ public static ChangeSet buildChangeSet(Table newTable, String sql, int toTableID) { ChangeSet.Builder builder = ChangeSet.newBuilder(); builder.setChangeLevel(ChangeLevel.TABLE.name()); assert(sql != null); builder.setSelectStatement(sql); builder.setTableId(newTable.getTableId()); builder.setOldSchema(newTable.getName().getSchemaName()); builder.setOldName(newTable.getName().getTableName()); builder.setNewSchema(newTable.getName().getSchemaName()); builder.setNewName(newTable.getName().getTableName()); builder.setToTableId(toTableID); return builder.build(); } /** ChangeSets for all tables affected by {@code newIndexes}. */ private static List<ChangeSet> buildChangeSets(AkibanInformationSchema ais, Collection<? extends Index> stubIndexes) { HashMultimap<Integer,Index> tableToIndexes = HashMultimap.create(); for(Index stub : stubIndexes) { // Re-look index up as external API previously only relied on names Table table = ais.getTable(stub.getIndexName().getFullTableName()); String stubName = stub.getIndexName().getName(); final Index index; switch(stub.getIndexType()) { case TABLE: index = table.getIndexIncludingInternal(stubName); break; case FULL_TEXT: index = table.getFullTextIndex(stubName); break; case GROUP: index = table.getGroup().getIndex(stubName); break; default: throw new IllegalStateException(stub.getIndexType().toString()); } assert (index != null) : stub; for(Integer tid : index.getAllTableIDs()) { tableToIndexes.put(tid, index); } } List<ChangeSet> changeSets = new ArrayList<>(); for(Entry<Integer, Collection<Index>> entry : tableToIndexes.asMap().entrySet()) { Table table = ais.getTable(entry.getKey()); ChangeSet.Builder builder = ChangeSet.newBuilder(); builder.setChangeLevel(ChangeLevel.INDEX.name()); builder.setTableId(table.getTableId()); builder.setOldSchema(table.getName().getSchemaName()); builder.setOldName(table.getName().getTableName()); builder.setNewSchema(table.getName().getSchemaName()); builder.setNewName(table.getName().getTableName()); for(Index index : entry.getValue()) { TableChanges.IndexChange.Builder indexChange = TableChanges.IndexChange.newBuilder(); indexChange.setChange(ChangeSetHelper.createAddChange(index.getIndexName().getName())); indexChange.setIndexType(index.getIndexType().name()); builder.addIndexChange(indexChange); } changeSets.add(builder.build()); } return changeSets; } /** ChangeSets for all tables affected, directly or indirectly, by {@code validator}. */ private static List<ChangeSet> buildChangeSets(AkibanInformationSchema oldAIS, AkibanInformationSchema newAIS, int changedTableID, TableChangeValidator validator) { Set<Integer> tableIDs = new HashSet<>(); for(ChangedTableDescription desc : validator.getState().descriptions) { tableIDs.add(desc.getTableID()); } // TABLE/GROUP change rebuilds entire group(s), so all are 'affected' if(validator.getFinalChangeLevel() == ChangeLevel.TABLE || validator.getFinalChangeLevel() == ChangeLevel.GROUP) { TableIDVisitor visitor = new TableIDVisitor(tableIDs); oldAIS.getTable(changedTableID).getGroup().visit(visitor); newAIS.getTable(changedTableID).getGroup().visit(visitor); } List<ChangeSet> changeSets = new ArrayList<>(); for(Integer tid : tableIDs) { ChangeSet.Builder cs = ChangeSet.newBuilder(); cs.setChangeLevel(validator.getFinalChangeLevel().name()); cs.setTableId(tid); final TableName oldName, newName; ChangedTableDescription desc = findByID(validator.getState().descriptions, tid); if(desc == null) { Table table = newAIS.getTable(tid); oldName = newName = table.getName(); } else { oldName = desc.getOldName(); newName = desc.getNewName(); } cs.setOldSchema(oldName.getSchemaName()); cs.setOldName(oldName.getTableName()); cs.setNewSchema(newName.getSchemaName()); cs.setNewName(newName.getTableName()); Table oldTable = oldAIS.getTable(tid); Table newTable = newAIS.getTable(tid); Set<String> handledIndexes = new HashSet<>(); // Only the table being directly modified has explicit change list if(tid == changedTableID) { // Full column information needed to create projects for new row for(TableChange cc : validator.getState().columnChanges) { cs.addColumnChange(ChangeSetHelper.createFromTableChange(cc)); } for(TableChange ic : validator.getState().tableIndexChanges) { TableChanges.IndexChange.Builder builder = TableChanges.IndexChange.newBuilder(); builder.setIndexType(IndexType.TABLE.name()); builder.setChange(ChangeSetHelper.createFromTableChange(ic)); cs.addIndexChange(builder); if(ic.getNewName() != null) { handledIndexes.add(ic.getNewName()); } } } Collection<TableIndex> additionalIndexes = Collections.emptyList(); if(desc != null) { additionalIndexes = findTableIndexesToBuild(desc, oldTable, newTable); } for(TableIndex index : additionalIndexes) { if(!handledIndexes.contains(index.getIndexName().getName())) { TableChanges.IndexChange.Builder builder = TableChanges.IndexChange.newBuilder(); builder.setIndexType(index.getIndexType().name()); String name = index.getIndexName().getName(); builder.setChange(ChangeSetHelper.createModifyChange(name, name)); cs.addIndexChange(builder); } } Group newGroup = newTable.getGroup(); for(String indexName : validator.getState().dataAffectedGI.keySet()) { GroupIndex index = newGroup.getIndex(indexName); if(newTable.getGroupIndexes().contains(index)) { TableChanges.IndexChange.Builder builder = TableChanges.IndexChange.newBuilder(); builder.setIndexType(index.getIndexType().name()); builder.setChange(ChangeSetHelper.createModifyChange(indexName, indexName)); cs.addIndexChange(builder); } } if(desc != null) { for(TableName seqName : desc.getDroppedSequences()) { cs.addIdentityChange(ChangeSetHelper.createDropChange(seqName.getTableName())); } for(String identityCol : desc.getIdentityAdded()) { Column c = newTable.getColumn(identityCol); assert (c != null) && (c.getIdentityGenerator() != null) : c; cs.addIdentityChange(ChangeSetHelper.createAddChange(c.getIdentityGenerator().getSequenceName().getTableName())); } } changeSets.add(cs.build()); } return changeSets; } private synchronized void onlineAt(OnlineDDLMonitor.Stage stage) { if(onlineDDLMonitor != null) { onlineDDLMonitor.at(stage); } } private Store store() { return store; } private SchemaManager schemaManager() { return schemaManager; } // // Internal Classes // private static class TableIDVisitor extends AbstractVisitor { private final Collection<Integer> tableIDs; private TableIDVisitor(Collection<Integer> tableIDs) { this.tableIDs = tableIDs; } @Override public void visit(Table table) { tableIDs.add(table.getTableId()); } } private static class AISValidatorPair { public final AkibanInformationSchema ais; public final TableChangeValidator validator; private AISValidatorPair(AkibanInformationSchema ais, TableChangeValidator validator) { this.ais = ais; this.validator = validator; } } }