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.List; import java.util.Map; import java.util.Set; import com.tesora.dve.common.catalog.CatalogDAO; import com.tesora.dve.common.catalog.CatalogEntity; import com.tesora.dve.common.catalog.Shape; import com.tesora.dve.common.catalog.UserTable; import com.tesora.dve.exceptions.PEException; import com.tesora.dve.lockmanager.LockType; import com.tesora.dve.queryplan.QueryStepDDLGeneralOperation.DDLCallback; import com.tesora.dve.server.connectionmanager.SSConnection; import com.tesora.dve.server.messaging.SQLCommand; import com.tesora.dve.sql.SchemaException; import com.tesora.dve.sql.ParserException.Pass; import com.tesora.dve.sql.expression.TableKey; import com.tesora.dve.sql.schema.DatabaseLock; import com.tesora.dve.sql.schema.PEAbstractTable; import com.tesora.dve.sql.schema.PEForeignKey; import com.tesora.dve.sql.schema.PEKey; import com.tesora.dve.sql.schema.PETable; import com.tesora.dve.sql.schema.SchemaContext; import com.tesora.dve.sql.schema.PEAbstractTable.TableCacheKey; import com.tesora.dve.sql.schema.cache.CacheInvalidationRecord; import com.tesora.dve.sql.schema.cache.InvalidationScope; import com.tesora.dve.sql.schema.mt.AdaptiveMTDDLPlannerUtils; import com.tesora.dve.sql.schema.mt.AdaptiveMultitenantSchemaPolicyContext; import com.tesora.dve.sql.schema.mt.FKRefMaintainer; import com.tesora.dve.sql.schema.mt.PETenant; import com.tesora.dve.sql.schema.mt.TableScope; import com.tesora.dve.sql.schema.mt.AdaptiveMTDDLPlannerUtils.DropTenantScope; import com.tesora.dve.sql.schema.mt.AdaptiveMTDDLPlannerUtils.CompositeNestedOperation; import com.tesora.dve.sql.schema.mt.TableScope.ScopeCacheKey; import com.tesora.dve.sql.transform.behaviors.BehaviorConfiguration; import com.tesora.dve.sql.transform.execution.ComplexDDLExecutionStep; import com.tesora.dve.sql.transform.execution.ExecutionSequence; import com.tesora.dve.sql.transform.execution.CatalogModificationExecutionStep.Action; import com.tesora.dve.sql.util.ListOfPairs; import com.tesora.dve.sql.util.ListSet; import com.tesora.dve.sql.util.Pair; import com.tesora.dve.variables.KnownVariables; import com.tesora.dve.worker.WorkerGroup; public class PEDropTenantTableStatement extends PEDropTableStatement { private TableScope scope; private PETenant tenant; public PEDropTenantTableStatement(PEDropTableStatement basedOn, TableScope ts, PETenant tenant) { super(basedOn); scope = ts; this.tenant = tenant; } // was well formed is now well formed action // yes yes can't happen // yes no flip to fixed // no no alter (make the referencing fk be forward) // no yes flip to shared // the notable exception is if the tenant is the landlord and the scope is null // in that case xlock the shape, figure out whether the table is still referenced, then drop it if possible. @Override public void plan(SchemaContext sc, ExecutionSequence es, BehaviorConfiguration config) throws PEException { if (tenant == null) { planLandlordDropTable(sc,es); } else { planTenantDropTable(sc,es); } } private void planLandlordDropTable(SchemaContext sc, ExecutionSequence es) throws PEException { TableKey tk = getDroppedTableKeys().get(0); PETable theTable = tk.getAbstractTable().asTable(); sc.beginSaveContext(); UserTable ut = null; try { ut = theTable.getPersistent(sc); } finally { sc.endSaveContext(); } DatabaseLock dl = new DatabaseLock("landlord drop table " + tk.getCacheKey(),theTable.getPEDatabase(sc)); sc.getConnection().acquireLock(dl, LockType.WSHARED); Shape s = ut.getShape(); ShapeLock sl = new ShapeLock("landlord drop table " + tk.getCacheKey(), s.getDatabase().getName(),s.getName(),s.getTypeHash()); // have to use exclusive to avoid tossing it underneath an ongoing operation sc.getConnection().acquireLock(sl, LockType.EXCLUSIVE); es.append(new ComplexDDLExecutionStep(theTable.getPEDatabase(sc),theTable.getStorageGroup(sc),theTable,Action.DROP, new DropTenantTableCallback(this,getSQLCommand(sc)))); } // regular tenant drop, rules for referencing tables. // was well formed is now well formed action // yes yes can't happen // yes no flip to fixed // no no alter (make the referencing fk be forward) // no yes flip to shared private void planTenantDropTable(SchemaContext sc, ExecutionSequence es) throws PEException { // note that because the table garbage collector is responsible for killing off tables we don't do it // so in the event that the subject table has no referring tables, this is a quick catalog update to // remove the scope from the scopes table PETable backingTable = scope.getTable(sc); CompositeNestedOperation uno = new CompositeNestedOperation(); uno.withChange(new DropTenantScope((ScopeCacheKey) scope.getCacheKey())); DropTableFKRefMaintainer maintenance = new DropTableFKRefMaintainer(tenant, backingTable, scope); maintenance.maintain(sc); maintenance.schedule(sc, es, uno); AdaptiveMTDDLPlannerUtils.addDDLCallback(sc, backingTable.getPEDatabase(sc), backingTable.getPersistentStorage(sc), backingTable, Action.CREATE, es, uno, 0L); } private static class DropTableFKRefMaintainer extends FKRefMaintainer { protected PETable subjectTable; protected TableScope subjectScope; public DropTableFKRefMaintainer(PETenant onTenant, PETable toDrop, TableScope theScope) { super(onTenant); subjectTable = toDrop; subjectScope = theScope; } @Override public ListOfPairs<TableScope, TaggedFK> computeRootSet(SchemaContext sc) { boolean required = KnownVariables.FOREIGN_KEY_CHECKS.getSessionValue(sc.getConnection().getVariableSource()).booleanValue(); // our root set is everything that refers to us List<TableScope> yonScopes = sc.findScopesForFKTargets(subjectTable, tenant); ListOfPairs<TableScope, TaggedFK> out = new ListOfPairs<TableScope, TaggedFK>(); for(TableScope ts : yonScopes) { PETable yonTable = ts.getTable(sc); for(PEKey pek : yonTable.getKeys(sc)) { if (!pek.isForeign()) continue; PEForeignKey pefk = (PEForeignKey) pek; if (pefk.isForward()) continue; if (pefk.getTargetTable(sc).getCacheKey().equals(subjectTable.getCacheKey())) { if (required) throw new SchemaException(Pass.PLANNER, "Unable to drop table " + subjectScope.getName().getQuoted() + " because referenced by foreign keys"); out.add(subjectScope,new TaggedFK(pefk,yonTable,ts)); } } } return out; } @Override public void modifyRoot(SchemaContext sc, Pair<TableScope, TaggedFK> r, Map<PETable, ModificationBlock> blocks) { PEForeignKey pefk = r.getSecond().getFK(); PETable enc = r.getSecond().getEnclosing(); ModificationBlock mb = blocks.get(enc); mb.unresolveFK(pefk, r.getFirst().getName().getUnqualified()); } } private static class DropTenantTableCallback extends DDLCallback { private final TableKey toDrop; private final String tableName; private final String databaseName; private final ListSet<CatalogEntity> deletedEntities; private final ListSet<CatalogEntity> updatedEntities; private final Set<PEAbstractTable<?>> referencedTables; private final CacheInvalidationRecord record; private final SQLCommand sql; public DropTenantTableCallback(PEDropTenantTableStatement orig, SQLCommand sql) { this.toDrop = orig.getDroppedTableKeys().get(0); this.tableName = toDrop.getAbstractTable().getName().getUnquotedName().get(); this.databaseName = ((TableCacheKey)toDrop.getAbstractTable().getCacheKey()).getDatabaseName(); this.deletedEntities = new ListSet<CatalogEntity>(); this.updatedEntities = new ListSet<CatalogEntity>(); this.referencedTables = new ListSet<PEAbstractTable<?>>(); this.record = new CacheInvalidationRecord(toDrop.getCacheKey(),InvalidationScope.CASCADE); this.sql = sql; } @Override public void beforeTxn(SSConnection conn, CatalogDAO c, WorkerGroup wg) throws PEException { deletedEntities.clear(); updatedEntities.clear(); } @Override public void inTxn(SSConnection conn, WorkerGroup wg) throws PEException { CatalogDAO c = conn.getCatalogDAO(); // just create a new schema context SchemaContext sc = SchemaContext.createContext(conn); sc.forceMutableSource(); PETable reloaded = (PETable) sc.getSource().find(sc, toDrop.getCacheKey()); if (reloaded == null) return; String command = "select count(*) from user_table ut inner join user_database ud on ut.user_database_id = ud.user_database_id " +"inner join scope s on s.scope_table_id = ut.table_id where ut.name = '%s' and ud.name = '%s'"; @SuppressWarnings("rawtypes") List results = c.nativeQuery( String.format(command,tableName,databaseName), Collections.<String, Object> emptyMap()); Object first = results.get(0); Number n = (Number) first; if (n.longValue() == 0) { List<TableKey> freshTKs = new ArrayList<TableKey>(); freshTKs.add(TableKey.make(sc,reloaded,1)); PEDropTableStatement.compute(sc, deletedEntities, updatedEntities, freshTKs, referencedTables, true); for (final PEAbstractTable<?> referenced : referencedTables) { this.record.addInvalidateAction(referenced.getCacheKey(), InvalidationScope.LOCAL); } } else { // still has references - this becomes a noop } } @Override public List<CatalogEntity> getUpdatedObjects() throws PEException { return updatedEntities; } @Override public List<CatalogEntity> getDeletedObjects() throws PEException { return deletedEntities; } @Override public SQLCommand getCommand(CatalogDAO c) { if (deletedEntities.isEmpty()) return SQLCommand.EMPTY; return sql; } @Override public boolean canRetry(Throwable t) { return AdaptiveMultitenantSchemaPolicyContext.canRetry(t); } @Override public boolean requiresFreshTxn() { return true; } @Override public String description() { return this.getClass().getSimpleName() + " on " + toDrop; } @Override public CacheInvalidationRecord getInvalidationRecord() { return record; } @Override public boolean requiresWorkers() { return true; } } }