package com.tesora.dve.sql.statement.ddl;
/*
* #%L
* Tesora Inc.
* Database Virtualization Engine
* %%
* Copyright (C) 2011 - 2014 Tesora Inc.
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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/>.
* #L%
*/
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.tesora.dve.common.catalog.*;
import org.apache.commons.collections.ListUtils;
import com.tesora.dve.db.DBResultConsumer;
import com.tesora.dve.exceptions.PECodingException;
import com.tesora.dve.exceptions.PEException;
import com.tesora.dve.lockmanager.LockType;
import com.tesora.dve.queryplan.ExecutionState;
import com.tesora.dve.queryplan.QueryStepDDLNestedOperation.NestedOperationDDLCallback;
import com.tesora.dve.queryplan.QueryStepFilterOperation.OperationFilter;
import com.tesora.dve.queryplan.QueryStepOperation;
import com.tesora.dve.resultset.ColumnSet;
import com.tesora.dve.server.connectionmanager.SSConnection;
import com.tesora.dve.server.messaging.SQLCommand;
import com.tesora.dve.sql.ParserException.Pass;
import com.tesora.dve.sql.SchemaException;
import com.tesora.dve.sql.expression.TableKey;
import com.tesora.dve.sql.node.expression.TableInstance;
import com.tesora.dve.sql.parser.InvokeParser;
import com.tesora.dve.sql.parser.ParserOptions;
import com.tesora.dve.sql.schema.ComplexPETable;
import com.tesora.dve.sql.schema.ConnectionValues;
import com.tesora.dve.sql.schema.LockInfo;
import com.tesora.dve.sql.schema.Name;
import com.tesora.dve.sql.schema.PEDatabase;
import com.tesora.dve.sql.schema.PEPersistentGroup;
import com.tesora.dve.sql.schema.PETable;
import com.tesora.dve.sql.schema.QualifiedName;
import com.tesora.dve.sql.schema.SchemaContext;
import com.tesora.dve.sql.schema.TableComponent;
import com.tesora.dve.sql.schema.UnqualifiedName;
import com.tesora.dve.sql.schema.cache.CacheInvalidationRecord;
import com.tesora.dve.sql.schema.cache.InvalidationScope;
import com.tesora.dve.sql.schema.cache.SchemaCacheKey;
import com.tesora.dve.sql.schema.modifiers.TableModifier;
import com.tesora.dve.sql.schema.types.Type;
import com.tesora.dve.sql.statement.ddl.alter.AddIndexAction;
import com.tesora.dve.sql.statement.ddl.alter.AlterTableAction;
import com.tesora.dve.sql.statement.ddl.alter.AlterTableAction.ClonableAlterTableAction;
import com.tesora.dve.sql.statement.ddl.alter.ConvertToAction;
import com.tesora.dve.sql.statement.ddl.alter.DropIndexAction;
import com.tesora.dve.sql.transform.behaviors.BehaviorConfiguration;
import com.tesora.dve.sql.transform.execution.CatalogModificationExecutionStep.Action;
import com.tesora.dve.sql.transform.execution.ComplexDDLExecutionStep;
import com.tesora.dve.sql.transform.execution.ExecutionSequence;
import com.tesora.dve.sql.transform.execution.ExecutionStep;
import com.tesora.dve.sql.transform.execution.FilterExecutionStep;
import com.tesora.dve.sql.transform.execution.IdentityConnectionValuesMap;
import com.tesora.dve.sql.transform.execution.ProjectingExecutionStep;
import com.tesora.dve.sql.transform.execution.SessionExecutionStep;
import com.tesora.dve.sql.transform.execution.SimpleDDLExecutionStep;
import com.tesora.dve.sql.util.Functional;
import com.tesora.dve.sql.util.ListOfPairs;
import com.tesora.dve.worker.MysqlTextResultCollector;
import com.tesora.dve.worker.WorkerGroup;
// an alter statement can have one or more actions. we either execute all of them or none of them.
public class PEAlterTableStatement extends PEAlterStatement<PETable> {
TableKey targetTableKey;
private List<AlterTableAction> actions;
private List<AlterTableAction> finalActions;
public PEAlterTableStatement(SchemaContext sc, TableKey target, List<AlterTableAction> alters) {
super(target.getAbstractTable().asTable(), false);
actions = alters;
targetTableKey = target;
finalActions = null;
for(AlterTableAction ata : actions) {
ata.setTarget(sc, target.getAbstractTable().asTable());
}
}
public PEAlterTableStatement(SchemaContext sc, TableKey target, AlterTableAction singleAlter) {
this(sc, target,Collections.singletonList(singleAlter));
}
@Override
public CacheInvalidationRecord getInvalidationRecord(SchemaContext pc) {
ListOfPairs<SchemaCacheKey<?>,InvalidationScope> clears = new ListOfPairs<SchemaCacheKey<?>,InvalidationScope>();
clears.add(targetTableKey.getAbstractTable().getPEDatabase(pc)
.getCacheKey(), InvalidationScope.CASCADE);
return new CacheInvalidationRecord(clears);
}
public TableKey getTableKey() {
return targetTableKey;
}
public void refresh(SchemaContext sc) {
backing = (PETable) backing.reload(sc);
for(AlterTableAction aa : actions)
aa.refresh(sc, backing);
}
public List<AlterTableAction> getActions() {
if (finalActions != null)
return finalActions;
return actions;
}
public boolean hasSQL(SchemaContext pc) {
for(AlterTableAction aa : actions) {
if (aa.hasSQL(pc, backing))
return true;
}
return false;
}
@Override
protected PETable modify(SchemaContext pc, PETable in) {
throw new SchemaException(Pass.PLANNER, "Invalid call to alter table statement.modify");
}
public boolean isNoop(SchemaContext pc, PETable pet) {
for(AlterTableAction aa : actions) {
if (!aa.isNoop(pc,pet))
return false;
}
return true;
}
public String isValid(SchemaContext pc, PETable pet) {
for(AlterTableAction aa : actions) {
String v = aa.isValid(pc,pet);
if (v != null) return v;
}
return null;
}
public PEAlterTableStatement adapt(SchemaContext pc, TableKey pet) {
ArrayList<AlterTableAction> newacts = new ArrayList<AlterTableAction>();
for(AlterTableAction aa : actions)
newacts.add(aa.adapt(pc,pet.getAbstractTable().asTable()));
return new PEAlterTableStatement(pc,pet,newacts);
}
@Override
public List<CatalogEntity> getDeleteObjects(SchemaContext pc) throws PEException {
ArrayList<CatalogEntity> ret = new ArrayList<CatalogEntity>();
List<AlterTableAction> use = new ArrayList<AlterTableAction>((finalActions != null ? finalActions : actions));
// if we have an drop index/add index where the defs are the same, elide the drop index
// we only do this if they are next to each other.
for(int i = 0; i < use.size(); i++) {
AlterTableAction ith = use.get(i);
if (ith instanceof DropIndexAction) {
DropIndexAction dia = (DropIndexAction) ith;
if ((i+1) < use.size()) {
AlterTableAction jth = use.get(i+1);
if (jth instanceof AddIndexAction) {
AddIndexAction aia = (AddIndexAction) jth;
String differs = dia.getKey().differs(pc, aia.getNewIndex(), true);
if (differs == null) {
use.remove(i);
i--;
}
}
}
}
}
for(AlterTableAction aa : use)
ret.addAll(aa.getDeleteObjects(pc));
return ret;
}
private AlterTableAction applyOneAction(SchemaContext pc, AlterTableAction aa, PETable tschema, PETable pschema) {
AlterTableAction add = aa.alterTable(pc,tschema);
if (pschema != null)
add = aa.alterTable(pc,pschema);
return add;
}
public void alterTable(SchemaContext pc, PETable tschema, PETable pschema) {
finalActions = new ArrayList<AlterTableAction>();
for(AlterTableAction aa : actions) {
AlterTableAction currentAction = aa;
while(currentAction != null) {
finalActions.add(currentAction);
currentAction = applyOneAction(pc, currentAction,tschema,pschema);
}
}
if (finalActions.size() == actions.size())
finalActions = null;
}
// override this to utilize the various functions
@Override
public List<CatalogEntity> getCatalogEntries(SchemaContext pc) throws PEException {
pc.beginSaveContext();
try {
backing.persistTree(pc,true);
PETable tschemaVersion = buildTSchemaTable(pc,backing);
// so the actions have to be done one at a time in case we have something like drop index, add index
finalActions = new ArrayList<AlterTableAction>();
for(AlterTableAction aa : actions) {
String v = aa.isValid(pc,backing);
if (v != null)
throw new PEException(v);
v = aa.isValid(pc, tschemaVersion);
if (v != null)
throw new PEException(v);
AlterTableAction currentAction = aa;
while(currentAction != null) {
finalActions.add(currentAction);
currentAction = applyOneAction(pc, currentAction,tschemaVersion,backing);
}
}
if (finalActions.size() == actions.size())
finalActions = null;
backing.setDeclaration(pc,tschemaVersion);
} finally {
pc.endSaveContext();
}
pc.beginSaveContext(true);
try {
backing.persistTree(pc);
return Functional.toList(pc.getSaveContext().getObjects());
} finally {
pc.endSaveContext();
}
}
// go back to the source, get a tschema version
public PETable buildTSchemaTable(SchemaContext pc, PETable persTable) throws PEException {
String cts = persTable.getDeclaration();
if (cts == null)
throw new PEException("No stored create table statement");
return persTable.recreate(pc, cts, new LockInfo(LockType.EXCLUSIVE, "alter table"));
}
@Override
public boolean filterStatement(SchemaContext pc) {
PETable tbl = getTarget();
if (tbl != null) {
if (pc.getConnection().isFilteredTable(
new QualifiedName(
tbl.getDatabase(pc).getName().getUnquotedName().getUnqualified(),
tbl.getName().getUnquotedName().getUnqualified()))) {
return true;
}
}
return false;
}
@Override
public void plan(SchemaContext sc, ExecutionSequence es, BehaviorConfiguration config) throws PEException {
final Set<ConvertToAction> complexPlanningActions = getAllActionsOfType(ConvertToAction.class);
if (complexPlanningActions.isEmpty()) {
super.plan(sc, es, config);
}
if (sc.getValues() == null)
sc.getValueManager().getValues(sc);
for (final ConvertToAction convertAction : complexPlanningActions) {
if (!convertAction.isTransientOnly()) {
final PETable targetTable = this.getTarget();
final ComplexAlterTableActionCallback actionCallback = new ComplexAlterTableActionCallback(targetTable, convertAction,
convertAction.getColumnMetadataContainer(),sc.getValues());
DistributionModel targetDistModel = targetTable.getDistributionVector(sc).getModel().getSingleton();
es.append(new ComplexDDLExecutionStep(targetTable.getPEDatabase(sc), targetTable.getStorageGroup(sc), targetTable, Action.ALTER, actionCallback, targetDistModel));
}
}
}
@SuppressWarnings("unchecked")
private <T extends AlterTableAction> Set<T> getAllActionsOfType(final Class<T> clazz) {
if (actions != null) {
final Set<T> actionsOfClass = new HashSet<T>();
for (final AlterTableAction action : actions) {
if (clazz.isAssignableFrom(action.getClass())) {
actionsOfClass.add((T) action);
}
}
return actionsOfClass;
}
return Collections.emptySet();
}
@Override
protected ExecutionStep buildStep(SchemaContext pc) throws PEException {
SQLCommand sql = getSQLCommand(pc);
List<CatalogEntity> newObjs = getCatalogObjects(pc);
List<CatalogEntity> delObjs = getDeleteObjects(pc);
if (finalActions != null)
sql = getSQLCommand(pc);
return new SimpleDDLExecutionStep(getDatabase(pc), getStorageGroup(pc), getRoot(), getAction(), sql,
delObjs, newObjs ,getInvalidationRecord(pc));
}
private static class ComplexAlterTableActionCallback extends NestedOperationDDLCallback {
private static class ColumnTypeMetadataCollector implements OperationFilter {
private final PETable forTable;
private final Map<String, Type> columnMetadata;
protected ColumnTypeMetadataCollector(final PETable forTable, final Map<String, Type> metadataContainer) {
if (forTable == null) {
throw new PECodingException("Must provide the source table.");
} else if (metadataContainer == null) {
throw new PECodingException("Must provide a container for the metadata.");
}
this.forTable = forTable;
this.columnMetadata = metadataContainer;
}
public FilterExecutionStep getCollectorExecutionStep(final SchemaContext sc) throws PEException {
return new FilterExecutionStep(ProjectingExecutionStep.build(sc, null, this.forTable.getStorageGroup(sc), this.buildSQL()), this);
}
private String buildSQL() {
return "SHOW COLUMNS IN " + this.forTable.getName().getSQL() + "";
}
@Override
public void filter(SSConnection ssCon, ColumnSet columnSet, List<ArrayList<String>> rowData, DBResultConsumer results) throws Throwable {
SchemaContext sc = SchemaContext.createContext(ssCon);
for (final List<String> row : rowData) {
final String columnName = row.get(0);
final ParserOptions options = ParserOptions.NONE.setDebugLog(true).setResolve().setFailEarly().setActualLiterals();
final Type columnType = InvokeParser.parseType(sc, options, row.get(1));
this.columnMetadata.put(columnName, columnType);
}
}
@Override
public String describe() {
return "ColumnTypeMetadataCollector";
}
}
private final PETable alterTarget;
private final ClonableAlterTableAction alterAction;
private final List<QueryStepOperation> plan = new ArrayList<QueryStepOperation>();
private final Map<String, Type> metadata;
private final ConnectionValues cv;
private PEAlterTableStatement alterTargetTableStatement;
protected ComplexAlterTableActionCallback(final PETable target, final ClonableAlterTableAction action,
final Map<String, Type> metadataContainer,
ConnectionValues cv) {
this.alterTarget = target;
this.alterAction = action;
this.metadata = metadataContainer;
this.cv = cv;
}
@Override
public void beforeTxn(SSConnection conn, CatalogDAO c, WorkerGroup wg) throws PEException {
final SchemaContext sc = SchemaContext.createContext(conn);
sc.forceMutableSource();
/*
* Build a sample table on which we then perform a trial conversion
* to get the resultant column types.
*/
final PETable sampleTarget = this.alterTarget.recreate(sc, this.alterTarget.getDeclaration(), new LockInfo(
com.tesora.dve.lockmanager.LockType.RSHARED, "transient alter table statement"));
final PEDatabase sampleTargetDatabase = sampleTarget.getPEDatabase(sc);
final List<TableModifier> sampleTargetModifiers = new ArrayList<TableModifier>();
for (final TableModifier entry : sampleTarget.getModifiers().getModifiers()) {
if (entry != null) {
sampleTargetModifiers.add(entry);
}
}
/* FKs would not resolve on the sample site. */
sampleTarget.removeForeignKeys(sc);
final List<TableComponent<?>> sampleTargetFieldsAndKeys = ListUtils.union(sampleTarget.getColumns(sc), sampleTarget.getKeys(sc));
final QualifiedName sampleTargetName = new QualifiedName(
sampleTargetDatabase.getName().getUnqualified(),
new UnqualifiedName(UserTable.getNewTempTableName())
);
final PEPersistentGroup sg = buildOneSiteGroup(sc, false);
/*
* Make sure the actual sample gets created as a TEMPORARY table in
* the user database so that it always gets removed.
*/
final ComplexPETable temporarySampleTarget = new ComplexPETable(sc,
sampleTargetName, sampleTargetFieldsAndKeys,
sampleTarget.getDistributionVector(sc), sampleTargetModifiers,
sg, sampleTargetDatabase, TableState.SHARED);
temporarySampleTarget.withTemporaryTable(sc);
final TableInstance targetTableInstance = new TableInstance(temporarySampleTarget, temporarySampleTarget.getName(), null, true);
final PECreateTableStatement createSampleTable = new PECreateTableStatement(temporarySampleTarget, false);
final PEAlterTableStatement alterSampleTable =
new PEAlterTableStatement(sc, targetTableInstance.getTableKey(), Collections.singletonList(this.alterAction.makeTransientOnlyClone()));
final ColumnTypeMetadataCollector metadataFilter = new ColumnTypeMetadataCollector(temporarySampleTarget, this.metadata);
final PEDropTableStatement dropSampleTable = new PEDropTableStatement(sc, Collections.singletonList(targetTableInstance.getTableKey()), Collections.<Name> emptyList(), true,
targetTableInstance.getTableKey().isUserlandTemporaryTable());
final ExecutionSequence es = new ExecutionSequence(null);
es.append(new SessionExecutionStep(null, sg, createSampleTable.getSQL(sc)));
es.append(new SessionExecutionStep(null, sg, alterSampleTable.getSQL(sc)));
es.append(metadataFilter.getCollectorExecutionStep(sc));
es.append(new SessionExecutionStep(null, sg, dropSampleTable.getSQL(sc)));
es.schedule(null, this.plan, null, sc, new IdentityConnectionValuesMap(cv), null);
}
@Override
public void inTxn(SSConnection conn, WorkerGroup wg) throws PEException {
final SchemaContext sc = SchemaContext.createContext(conn);
sc.forceMutableSource();
final TableInstance targetTableInstance = new TableInstance(this.alterTarget, this.alterTarget.getName(), null, true);
this.alterTargetTableStatement = new PEAlterTableStatement(sc, targetTableInstance.getTableKey(),
Collections.<AlterTableAction> singletonList(this.alterAction));
final ExecutionSequence es = new ExecutionSequence(null);
es.append(new SessionExecutionStep(this.alterTarget.getDatabase(sc), this.alterTarget.getStorageGroup(sc), this.alterTargetTableStatement
.getSQL(sc)));
es.schedule(null, this.plan, null, sc, new IdentityConnectionValuesMap(sc.getValues()),null);
}
@Override
public boolean canRetry(Throwable t) {
return false;
}
@Override
public boolean requiresFreshTxn() {
return false;
}
@Override
public String description() {
return "Perform an ALTER operation on a native table and collect the column metadata.";
}
@Override
public CacheInvalidationRecord getInvalidationRecord() {
return new CacheInvalidationRecord(this.alterTarget.getCacheKey(), InvalidationScope.LOCAL);
}
@Override
public boolean requiresWorkers() {
return true;
}
@Override
public void executeNested(ExecutionState estate, WorkerGroup wg, DBResultConsumer resultConsumer) throws Throwable {
for (final QueryStepOperation qso : this.plan) {
qso.executeSelf(estate, wg, new MysqlTextResultCollector());
}
// The metadata should have already been loaded, so update the
// catalog records.
final SchemaContext sc = SchemaContext.createContext(estate.getConnection());
this.alterTargetTableStatement.getCatalogEntries(sc);
this.alterTargetTableStatement.getDeleteObjects(sc);
}
}
}