package com.tesora.dve.sql.schema.mt; /* * #%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.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import com.tesora.dve.common.catalog.CatalogDAO; import com.tesora.dve.common.catalog.CatalogEntity; import com.tesora.dve.common.catalog.IndexType; import com.tesora.dve.common.catalog.Key; import com.tesora.dve.common.catalog.KeyColumn; import com.tesora.dve.common.catalog.Shape; import com.tesora.dve.common.catalog.TableState; import com.tesora.dve.common.catalog.TableVisibility; import com.tesora.dve.common.catalog.UserDatabase; import com.tesora.dve.common.catalog.UserTable; import com.tesora.dve.db.DBEmptyTextResultConsumer; import com.tesora.dve.db.DBResultConsumer; import com.tesora.dve.exceptions.PEException; import com.tesora.dve.lockmanager.LockType; import com.tesora.dve.queryplan.ExecutionState; import com.tesora.dve.queryplan.QueryStepDDLGeneralOperation; import com.tesora.dve.queryplan.QueryStepDDLNestedOperation.NestedOperationDDLCallback; import com.tesora.dve.queryplan.QueryStepOperation; import com.tesora.dve.server.connectionmanager.SSConnection; import com.tesora.dve.server.global.HostService; import com.tesora.dve.server.messaging.SQLCommand; import com.tesora.dve.singleton.Singletons; import com.tesora.dve.sql.expression.MTTableKey; import com.tesora.dve.sql.expression.TableKey; import com.tesora.dve.sql.node.expression.ColumnInstance; import com.tesora.dve.sql.node.expression.ExpressionAlias; import com.tesora.dve.sql.node.expression.ExpressionNode; import com.tesora.dve.sql.node.expression.LiteralExpression; import com.tesora.dve.sql.node.expression.NameAlias; import com.tesora.dve.sql.node.expression.TableInstance; import com.tesora.dve.sql.parser.ParserOptions; import com.tesora.dve.sql.schema.LockInfo; import com.tesora.dve.sql.schema.Name; import com.tesora.dve.sql.schema.PEAbstractTable.TableCacheKey; import com.tesora.dve.sql.schema.PEColumn; import com.tesora.dve.sql.schema.PEDatabase; import com.tesora.dve.sql.schema.PEForeignKey; import com.tesora.dve.sql.schema.PEForeignKeyColumn; import com.tesora.dve.sql.schema.PEKey; import com.tesora.dve.sql.schema.PEKeyColumn; import com.tesora.dve.sql.schema.PEKeyColumnBase; import com.tesora.dve.sql.schema.PEStorageGroup; import com.tesora.dve.sql.schema.PETable; import com.tesora.dve.sql.schema.Persistable; import com.tesora.dve.sql.schema.SchemaContext; 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.mt.PETenant.TenantCacheKey; import com.tesora.dve.sql.schema.mt.TableScope.ScopeCacheKey; import com.tesora.dve.sql.statement.ddl.PEAlterTableStatement; import com.tesora.dve.sql.statement.ddl.PEAlterTenantTableStatement; import com.tesora.dve.sql.statement.ddl.ShapeLock; 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.DropIndexAction; import com.tesora.dve.sql.statement.dml.AliasInformation; import com.tesora.dve.sql.statement.dml.DeleteStatement; import com.tesora.dve.sql.statement.dml.InsertIntoSelectStatement; import com.tesora.dve.sql.statement.dml.SelectStatement; 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.IdentityConnectionValuesMap; import com.tesora.dve.sql.transform.execution.SessionExecutionStep; import com.tesora.dve.sql.util.Functional; import com.tesora.dve.sql.util.ListOfPairs; import com.tesora.dve.sql.util.Pair; import com.tesora.dve.sql.util.UnaryFunction; import com.tesora.dve.worker.WorkerGroup; public final class AdaptiveMTDDLPlannerUtils { public static void addDDLCallback(SchemaContext sc, PEDatabase pdb, PEStorageGroup psg, Persistable<?,?> theRoot, Action theAction, ExecutionSequence es, QueryStepDDLGeneralOperation.DDLCallback cb, Long updateCountOverride) { ComplexDDLExecutionStep step = new ComplexDDLExecutionStep(pdb, psg, theRoot, theAction, cb); if (updateCountOverride != null) step.setUpdateCountOverride(updateCountOverride); es.append(step); } public static CreateTableOperation addCreateTable(SchemaContext sc, ExecutionSequence es, PETenant theTenant, PETable theTable, UnqualifiedName visibleName, TableState requestedState, Map<UnqualifiedName,LateFKFixup> lateFixups) { CreateTableOperation csto = new CreateTableOperation(sc, theTable,visibleName, theTenant, requestedState, lateFixups); addDDLCallback(sc,theTable.getPEDatabase(sc), theTable.getStorageGroup(sc), theTable, Action.CREATE, es,csto, null); return csto; } // when a table is referenced and the referencing fk has a set null action, we need to ensure that the // target table has a naked index public static void handleSetNullActions(final SchemaContext sc, ExecutionSequence es, PETable referring, TableScope referringScope, PETenant onTenant, Map<UnqualifiedName,LateFKFixup> forwarding) throws PEException { ListOfPairs<PETable,PEForeignKey> referringKeys = new ListOfPairs<PETable,PEForeignKey>(); ListOfPairs<PETable,PEKey> added = new ListOfPairs<PETable,PEKey>(); for(PEKey pek : referring.getKeys(sc)) { if (!pek.isForeign()) continue; PEForeignKey pefk = (PEForeignKey) pek; if (pefk.isForward()) continue; Pair<PEForeignKey,PEKey> nk = maybeRequiresNewKey(sc,pefk); if (nk != null) { PETable target = nk.getFirst().getTargetTable(sc); added.add(target,nk.getSecond()); referringKeys.add(target,nk.getFirst()); } } if (added.isEmpty()) return; for(int i = 0; i < added.size(); i++) { Pair<PETable,PEKey> nkp = added.get(i); Pair<PETable,PEForeignKey> nsp = referringKeys.get(i); AlterTableAction ata = new AddIndexAction(nkp.getSecond()); TableKey ntk = TableKey.make(sc, nkp.getFirst(), sc.getNextTable()); PEAlterTableStatement peats = new PEAlterTableStatement(sc,ntk,ata); PEAlterTenantTableStatement peatts = new PEAlterTenantTableStatement(sc, peats, sc.getPolicyContext().getOfTenant(nkp.getFirst().getPersistent(sc)),onTenant); peatts.plan(sc, es, sc.getBehaviorConfiguration()); forwarding.put(nsp.getSecond().getSymbol(),new LateFKFixup(nsp.getSecond().getSymbol(),peatts.getFinalTableName(),"add nontenant key for set null fk")); } } public static Pair<PEForeignKey,PEKey> maybeRequiresNewKey(final SchemaContext sc, PEForeignKey pefk) { PETable target = pefk.getTargetTable(sc); List<PEKeyColumnBase> keyCols = pefk.getKeyColumns(); List<PEForeignKeyColumn> pefkcs = Functional.apply(keyCols, new UnaryFunction<PEForeignKeyColumn,PEKeyColumnBase>() { @Override public PEForeignKeyColumn evaluate(PEKeyColumnBase object) { return (PEForeignKeyColumn)object; } }); PEKey matching = null; for(PEKey ik : target.getKeys(sc)) { if (ik.isForeign()) continue; List<PEColumn> cols = ik.getColumns(sc); if (cols.size() != pefkcs.size()) continue; boolean all = true; for(int i = 0; i < pefkcs.size(); i++) { if (!cols.get(i).getName().equals(pefkcs.get(i).getTargetColumnName())) { all = false; break; } } if (!all) continue; matching = ik; break; } if (matching == null) { // have to alter the target table to add an index List<PEKeyColumnBase> nkc = Functional.apply(pefkcs, new UnaryFunction<PEKeyColumnBase,PEForeignKeyColumn>() { @Override public PEKeyColumn evaluate(PEForeignKeyColumn object) { return new PEKeyColumn(object.getTargetColumn(sc),null,-1L); } }); PEKey npek = new PEKey(null,IndexType.BTREE,nkc,null); // this key is always hidden npek.setHidden(true); return new Pair<PEForeignKey, PEKey>(pefk,npek); } return null; } public static Pair<PEForeignKey,PEKey> maybeRequiresNewKey(final SchemaContext sc, PEForeignKey pefk, PETable target) { List<PEKeyColumnBase> keyCols = pefk.getKeyColumns(); List<PEForeignKeyColumn> pefkcs = Functional.apply(keyCols, new UnaryFunction<PEForeignKeyColumn,PEKeyColumnBase>() { @Override public PEForeignKeyColumn evaluate(PEKeyColumnBase object) { return (PEForeignKeyColumn)object; } }); PEKey matching = null; for(PEKey ik : target.getKeys(sc)) { if (ik.isForeign()) continue; List<PEColumn> cols = ik.getColumns(sc); if (cols.size() != pefkcs.size()) continue; boolean all = true; for(int i = 0; i < pefkcs.size(); i++) { if (!cols.get(i).getName().equals(pefkcs.get(i).getTargetColumnName())) { all = false; break; } } if (!all) continue; matching = ik; break; } if (matching == null) { // have to alter the target table to add an index List<PEKeyColumnBase> nkc = new ArrayList<PEKeyColumnBase>(); for(PEForeignKeyColumn pefkc : pefkcs) { UnqualifiedName unq = pefkc.getTargetColumnName(); PEColumn pec = target.lookup(sc, unq); nkc.add(new PEKeyColumn(pec,null,-1L)); } PEKey npek = new PEKey(null,IndexType.BTREE,nkc,null); // this key is always hidden npek.setHidden(true); return new Pair<PEForeignKey, PEKey>(pefk,npek); } return null; } public interface LazyAllocatedTable { // this will have the right name PETable getTable(SchemaContext sc); String describe(); boolean isWellFormed(); } public static class DirectAllocatedTable implements LazyAllocatedTable { private final TableCacheKey theKey; private final boolean wellFormed; public DirectAllocatedTable(TableCacheKey tck, boolean wf) { theKey = tck; wellFormed = wf; } @Override public PETable getTable(SchemaContext sc) { return theKey.load(sc).asTable(); } @Override public String describe() { return "DirectAllocatedTable for " + theKey; } @Override public boolean isWellFormed() { return wellFormed; } } // create table operation is always a separate operation because we need the persistent table id // so we can reference it elsewhere public static class CreateTableOperation extends QueryStepDDLGeneralOperation.DDLCallback implements LazyAllocatedTable { private final PETable definition; private final PETenant originator; private final TableState requestedState; private TableCacheKey targetTable; private final UnqualifiedName originalName; private SQLCommand sql; private final List<CatalogEntity> updates; private final Map<UnqualifiedName,LateFKFixup> lateFKFixups; private boolean haveLock = false; private UnqualifiedName generatedName = null; public CreateTableOperation(SchemaContext sc, PETable def, UnqualifiedName logicalName, PETenant requestingTenant, TableState state, Map<UnqualifiedName,LateFKFixup> fixups) { this.definition = def; this.originalName = logicalName; this.updates = new ArrayList<CatalogEntity>(); this.originator = requestingTenant; this.requestedState = state; this.lateFKFixups = fixups; if (fixups != null && state == TableState.SHARED) { for(PEKey pek : def.getKeys(sc)) { if (!pek.isForeign()) continue; PEForeignKey pefk = (PEForeignKey) pek; LateFKFixup any = fixups.get(pefk.getSymbol()); if (any != null) { if (any.getTargetTable() == null) throw new IllegalStateException("Creating shared table " + originalName + " with dangling fixup"); else if (!any.getTargetTable().isWellFormed()) throw new IllegalStateException("Creating shared table " + originalName + " with fixed fixup"); } } } targetTable = null; } public UnqualifiedName getLogicalName() { return originalName; } public PETable getDefinition() { return definition; } public TableState getState() { return requestedState; } @Override public PETable getTable(SchemaContext sc) { return sc.getSource().find(sc, targetTable).asTable(); } private PETable recreateTable(SchemaContext sc) { sc.setOptions(ParserOptions.NONE.setAllowTenantColumn().disableMTLookupChecks()); PETable rc = definition.recreate(sc, definition.getDeclaration(), new LockInfo(LockType.EXCLUSIVE, "adaptive mt ddl (fks)")); if (!lateFKFixups.isEmpty()) { boolean changed = false; for(PEKey pek : rc.getKeys(sc)) { if (!pek.isForeign()) continue; PEForeignKey pefk = (PEForeignKey) pek; LateFKFixup any = lateFKFixups.get(pefk.getSymbol()); if (any != null) { any.fixupForeignKey(sc, pefk); changed = true; } } if (changed) { rc.setDeclaration(sc, rc); } } return rc; } @Override public void beforeTxn(SSConnection ssCon, CatalogDAO c, WorkerGroup wg) throws PEException { updates.clear(); targetTable = null; if (!haveLock) { SchemaContext sc = SchemaContext.createContext(ssCon); sc.forceMutableSource(); PETable theTable = recreateTable(sc); PEDatabase pdb = theTable.getPEDatabase(sc); UserDatabase ofdb = pdb.getPersistent(sc); String shapeName = originalName.get(); String hash = theTable.getTypeHash(); Shape s = c.findShape(ofdb, shapeName, theTable.getDefinition(), hash); // take a shape lock here; if the shape exists we can use a shared lock. // otherwise it must be exclusive to avoid dup shapes in the catalog. ShapeLock theShapeLock = new ShapeLock("create tenant table " + originalName + " on tenant " + originator.getName(),sc,originalName.getUnqualified().getUnquotedName().get(),theTable); ssCon.acquireLock(theShapeLock, (s == null ? LockType.EXCLUSIVE : LockType.WSHARED)); haveLock = true; } } @Override public void inTxn(SSConnection conn, WorkerGroup wg) throws PEException { CatalogDAO c = conn.getCatalogDAO(); SchemaContext sc = SchemaContext.createContext(conn); sc.forceMutableSource(); PETable nt = recreateTable(sc); PEDatabase pdb = nt.getPEDatabase(sc); UserDatabase ofdb = pdb.getPersistent(sc); String shapeName = originalName.get(); String hash = nt.getTypeHash(); Shape s = c.findShape(ofdb, shapeName, nt.getDefinition(), hash); if (s != null) { if (requestedState == TableState.SHARED) { c.refreshForLock(s); List<UserTable> candidates = c.findMatchingTables(s,TableState.SHARED); UserTable existingSharedTable = (candidates.isEmpty() ? null : candidates.get(0)); if (existingSharedTable != null) targetTable = (TableCacheKey) PETable.getTableKey(existingSharedTable); } } else { c.refreshForLock(ofdb); s = c.findShape(ofdb, shapeName, nt.getDefinition(), hash); if (s == null) { s = new Shape(ofdb, shapeName, nt.getDefinition(), hash); updates.add(s); } } if (targetTable == null) { nt.setState(requestedState); if (generatedName == null) { PETenant reloaded = (PETenant) sc.getSource().find(sc, originator.getCacheKey()); generatedName = reloaded.buildPrivateTableName(sc, originalName); } nt.resetName(sc, generatedName); nt.setDeclaration(sc, nt); if (requestedState == TableState.SHARED) { for(PEKey pek : nt.getKeys(sc)) { if (!pek.isForeign()) continue; PEForeignKey pefk = (PEForeignKey) pek; if (pefk.isForward()) throw new IllegalStateException("Creating shared table " + generatedName + " with dangling fk " + pefk.getSymbol()); PETable targ = pefk.getTargetTable(sc); if (targ.getState() == TableState.FIXED) throw new IllegalStateException("Creating shared table " + generatedName + " with fk " + pefk.getSymbol() + " targeting fixed table " + targ.getName()); } } sc.beginSaveContext(); UserTable thePersistentTable = null; try { thePersistentTable = nt.persistTree(sc); updates.addAll(sc.getSaveContext().getObjects()); } finally { sc.endSaveContext(); } thePersistentTable.setShape(s); sql = new SQLCommand(conn, nt.getDeclaration()); targetTable = (TableCacheKey) PETable.getTableKey(pdb, generatedName); } else { sql = SQLCommand.EMPTY; } } @Override public List<CatalogEntity> getUpdatedObjects() throws PEException { return updates; } @Override public SQLCommand getCommand(CatalogDAO c) { return (sql == null ? SQLCommand.EMPTY : 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() + " for table named " + definition.getName(); } @Override public CacheInvalidationRecord getInvalidationRecord() { // new table, no invalidation record return null; } @Override public boolean requiresWorkers() { return true; } @Override public String describe() { return description(); } @Override public void postCommitAction(CatalogDAO c) throws PEException { CatalogSanity.assertCatalogSanity(c); } @Override public boolean isWellFormed() { return requestedState == TableState.SHARED; } } public interface ChangeSource extends LazyAllocatedTable { public List<QueryStepOperation> getDDLSteps(); public List<QueryStepOperation> getDMLSteps(); public void beforeTxn(SSConnection ssConn); public void inTxn(SchemaContext sc, CatalogDAO c) throws PEException; public CacheInvalidationRecord getInvalidationRecord(); public List<CatalogEntity> getUpdates(); public List<CatalogEntity> getDeletes(); public boolean requiresWorkers(); public QueryStepOperation getCanonicalResultStep(); } public static class CompositeNestedOperation extends NestedOperationDDLCallback { protected final List<ChangeSource> changes; protected List<CatalogEntity> updates; protected List<CatalogEntity> deletes; protected List<QueryStepOperation> steps; protected QueryStepOperation canonicalResultStep; protected CacheInvalidationRecord record; public CompositeNestedOperation() { this.changes = new ArrayList<ChangeSource>(); } public CompositeNestedOperation withChange(ChangeSource cs) { this.changes.add(cs); return this; } public CompositeNestedOperation withChanges(List<ChangeSource> multi) { this.changes.addAll(multi); return this; } @Override public void beforeTxn(SSConnection ssCon, CatalogDAO c, WorkerGroup wg) throws PEException { for(ChangeSource cs : changes) cs.beforeTxn(ssCon); updates = null; deletes = null; steps = null; } @Override public void inTxn(SSConnection conn, WorkerGroup wg) throws PEException { CatalogDAO c = conn.getCatalogDAO(); SchemaContext sc = SchemaContext.createContext(conn); sc.forceMutableSource(); for(ChangeSource cs : changes) cs.inTxn(sc,c); // for both updates and deletes we are going to preserve the order they come off the changes LinkedHashSet<CatalogEntity> nodupes = new LinkedHashSet<CatalogEntity>(); for(ChangeSource cs : changes) nodupes.addAll(cs.getUpdates()); updates = Functional.toList(nodupes); nodupes.clear(); for(ChangeSource cs : changes) nodupes.addAll(cs.getDeletes()); deletes = Functional.toList(nodupes); steps = new ArrayList<QueryStepOperation>(); for(ChangeSource cs : changes) accumulateSteps(cs,cs.getDDLSteps()); for(ChangeSource cs : changes) accumulateSteps(cs,cs.getDMLSteps()); } private void accumulateSteps(ChangeSource cs, List<QueryStepOperation> t) { if (cs.getCanonicalResultStep() != null) canonicalResultStep = cs.getCanonicalResultStep(); steps.addAll(t); } @Override public List<CatalogEntity> getUpdatedObjects() throws PEException { return updates; } @Override public List<CatalogEntity> getDeletedObjects() throws PEException { return deletes; } @Override public boolean canRetry(Throwable t) { return false; } @Override public boolean requiresFreshTxn() { return false; } @Override public String description() { StringBuilder buf = new StringBuilder(); buf.append("CompositeNestedOperation{"); buf.append(Functional.join(changes, ",", new UnaryFunction<String,ChangeSource>() { @Override public String evaluate(ChangeSource object) { return object.describe(); } })); buf.append("}"); return buf.toString(); } @Override public CacheInvalidationRecord getInvalidationRecord() { if (record == null) { ListOfPairs<SchemaCacheKey<?>,InvalidationScope> all = new ListOfPairs<SchemaCacheKey<?>,InvalidationScope>(); for(ChangeSource cs : changes) { CacheInvalidationRecord sub = cs.getInvalidationRecord(); if (sub == null) continue; all.addAll(sub.getInvalidateActions()); } record = new CacheInvalidationRecord(all); } return record; } @Override public void executeNested(ExecutionState estate, WorkerGroup wg, DBResultConsumer resultConsumer) throws Throwable { for(QueryStepOperation qso : steps) { boolean care = (qso == canonicalResultStep || (canonicalResultStep == null && qso == steps.get(steps.size() - 1))); qso.executeSelf(estate, wg, (care ? resultConsumer : DBEmptyTextResultConsumer.INSTANCE)); } } @Override public boolean requiresWorkers() { for (ChangeSource cs : changes) if (cs.requiresWorkers()) return true; return false; } @Override public void postCommitAction(CatalogDAO c) throws PEException { try { CatalogSanity.assertCatalogSanity(c); } catch (PEException pe) { throw new PEException(description(),pe); } } } public static class TableFlip implements ChangeSource { private final TableScope scope; private final TableCacheKey srcTable; private final LazyAllocatedTable targetTable; private final List<CatalogEntity> updates; private final List<QueryStepOperation> steps; private final CacheInvalidationRecord record; private final boolean isCanonical; private QueryStepOperation canonicalStep; public TableFlip(TableScope theScope, TableCacheKey theSrcTab, LazyAllocatedTable theTargetTab, boolean canonicalResults, CacheInvalidationRecord cacheClear) { this.scope = theScope; this.srcTable = theSrcTab; this.targetTable = theTargetTab; this.updates = new ArrayList<CatalogEntity>(); this.steps = new ArrayList<QueryStepOperation>(); this.record = cacheClear; this.isCanonical = canonicalResults; } @Override public PETable getTable(SchemaContext sc) { return targetTable.getTable(sc); } @Override public List<QueryStepOperation> getDDLSteps() { return Collections.emptyList(); } @Override public List<QueryStepOperation> getDMLSteps() { return steps; } @Override public void beforeTxn(SSConnection ssCon) { steps.clear(); updates.clear(); } @Override public void inTxn(SchemaContext sc, CatalogDAO c) throws PEException { PETable srcTab = sc.getSource().find(sc, srcTable).asTable(); PETable targetTab = targetTable.getTable(sc); steps.addAll(buildSteps(sc,scope,srcTab,targetTab)); TableVisibility tv = scope.getPersistent(sc); UserTable targetUserTable = targetTab.getPersistent(sc); tv.setTable(targetUserTable); updates.add(tv); Singletons.require(HostService.class).onGarbageEvent(); } private List<QueryStepOperation> buildSteps(SchemaContext sc, TableScope scope, PETable currentTable, PETable newTable) throws PEException { ExecutionSequence es = new ExecutionSequence(null); Map<PEColumn, PEColumn> columnMap = buildForwarding(sc, currentTable,newTable); // allocate a new table instance TableKey targtk = new MTTableKey(newTable,scope,sc.getNextTable()); TableInstance targti = targtk.toInstance(); List<ExpressionNode> targColumns = getColumns(sc, targti, columnMap); if (newTable.getColumns(sc).size() > currentTable.getColumns(sc).size() && targColumns.size() < newTable.getColumns(sc).size()) throw new PEException("Missing constant value in src select"); // build a select against the source table for the source tenant SelectStatement srcSelect = buildSourceSelect(sc,scope,currentTable,targColumns,columnMap); // apply the mt filter in order to set the correct tenant id on it - not the current tenant id AdaptiveMultitenantSchemaPolicyContext mspc = (AdaptiveMultitenantSchemaPolicyContext) sc.getPolicyContext(); mspc.applyDegenerateMultitenantFilter(srcSelect, LiteralExpression.makeAutoIncrLiteral(scope.getTenant(sc).getTenantID())); InsertIntoSelectStatement iiss = new InsertIntoSelectStatement(targti,targColumns,srcSelect,false,null,(AliasInformation)null,null); iiss.plan(sc, es, sc.getBehaviorConfiguration()); DeleteStatement ds = AdaptiveMultitenantSchemaPolicyContext.buildTenantDeleteFromTableStatement(sc, currentTable, scope); ds.plan(sc,es, sc.getBehaviorConfiguration()); List<QueryStepOperation> allsteps = new ArrayList<QueryStepOperation>(); es.schedule(null, allsteps, null, sc, new IdentityConnectionValuesMap(sc.getValues()),null); if (isCanonical) canonicalStep = allsteps.get(0); return allsteps; } // target may have more columns, or fewer columns, than src. fill in the appropriate columns // with the default expression private SelectStatement buildSourceSelect(SchemaContext pc, TableScope scope, PETable srctab, List<ExpressionNode> matchColumns, Map<PEColumn,PEColumn> forwarding) { TableKey srctk = new MTTableKey(srctab,scope,pc.getNextTable()); TableInstance ti = srctk.toInstance(); List<ExpressionNode> proj = new ArrayList<ExpressionNode>(); AliasInformation ai = new AliasInformation(); for(ExpressionNode te : matchColumns) { ColumnInstance tci = (ColumnInstance)te; PEColumn tc = tci.getPEColumn(); PEColumn sc = forwarding.get(tc); ExpressionNode value = null; if (sc != null) { value = new ColumnInstance(sc,ti); } else if (!tc.isAutoIncrement()) { if (tc.isNullable()) value = LiteralExpression.makeNullLiteral(); else value = tc.getType().getZeroValueLiteral(); } if (value != null) { ExpressionAlias ea = new ExpressionAlias(value,new NameAlias(tc.getName().getUnqualified()),false); ai.addAlias(tc.getName().getUnqualified().getUnquotedName().get()); proj.add(ea); } else { } } SelectStatement ss = new SelectStatement(ai) .setTables(ti) .setProjection(proj) .setLocking(); ss.getDerivedInfo().addLocalTable(ti.getTableKey()); ss.normalize(pc); return ss; } private List<ExpressionNode> getColumns(SchemaContext pc, final TableInstance ti, Map<PEColumn, PEColumn> forwarding) { PETable pet = (PETable) ti.getTable(); List<ExpressionNode> out = new ArrayList<ExpressionNode>(); for(PEColumn tc : pet.getColumns(pc)) { PEColumn sc = forwarding.get(tc); if (sc != null || !tc.isAutoIncrement()) { out.add(new ColumnInstance(tc,ti)); } } return out; } // build a map from target columns to source columns private Map<PEColumn, PEColumn> buildForwarding(SchemaContext pc, PETable src, PETable target) { // figure out the column forwarding - 4 cases // [1] column is removed // [2] column is added // [3] column is renamed // [4] column is changed but not renamed // first two - number of columns between src and target differ // last two - same - can do a direct map as column definition order would not change HashMap<PEColumn, PEColumn> out = new HashMap<PEColumn, PEColumn>(); List<PEColumn> srccols = src.getColumns(pc); List<PEColumn> targcols = target.getColumns(pc); if (srccols.size() == targcols.size()) { for(int i = 0; i < srccols.size(); i++) { out.put(targcols.get(i),srccols.get(i)); } return out; } else { for(PEColumn tc : targcols) { PEColumn sc = src.lookup(pc,tc.getName()); out.put(tc, sc); } return out; } } @Override public CacheInvalidationRecord getInvalidationRecord() { return record; } @Override public List<CatalogEntity> getUpdates() { return updates; } @Override public List<CatalogEntity> getDeletes() { return Collections.emptyList(); } @Override public boolean requiresWorkers() { // most assuredly return true; } @Override public QueryStepOperation getCanonicalResultStep() { return canonicalStep; } @Override public String describe() { return "TableFlip from " + srcTable + " on scope " + scope.getCacheKey() + " to " + targetTable.describe(); } @Override public boolean isWellFormed() { return targetTable.isWellFormed(); } } public static class CreateTenantScope implements ChangeSource { private final List<CatalogEntity> updates; private final TenantCacheKey tenantKey; private final UnqualifiedName logicalName; private final Long autoIncOffset; private final LazyAllocatedTable backingTableName; public CreateTenantScope(TenantCacheKey tenant, UnqualifiedName visibleName, Long anyAutoInc, LazyAllocatedTable backingTableName) { this.tenantKey = tenant; this.logicalName = visibleName; this.autoIncOffset = anyAutoInc; this.backingTableName = backingTableName; this.updates = new ArrayList<CatalogEntity>(); } @Override public List<QueryStepOperation> getDDLSteps() { return Collections.emptyList(); } @Override public List<QueryStepOperation> getDMLSteps() { return Collections.emptyList(); } @Override public void beforeTxn(SSConnection ssCon) { updates.clear(); } @Override public void inTxn(SchemaContext sc, CatalogDAO c) throws PEException { PETable backing = backingTableName.getTable(sc); PETenant tenant = sc.getSource().find(sc, tenantKey); TableScope ts = tenant.setVisible(sc, backing, logicalName, autoIncOffset, new LockInfo(LockType.EXCLUSIVE,"mtddl")); sc.beginSaveContext(); try { ts.persistTree(sc); updates.addAll(sc.getSaveContext().getObjects()); } finally { sc.endSaveContext(); } } @Override public CacheInvalidationRecord getInvalidationRecord() { return null; } @Override public List<CatalogEntity> getUpdates() { return updates; } @Override public List<CatalogEntity> getDeletes() { return Collections.emptyList(); } @Override public boolean requiresWorkers() { return false; } @Override public PETable getTable(SchemaContext sc) { return backingTableName.getTable(sc); } @Override public QueryStepOperation getCanonicalResultStep() { return null; } @Override public String describe() { return "CreateTenantScope " + logicalName + " on " + tenantKey; } @Override public boolean isWellFormed() { return backingTableName.isWellFormed(); } } public static class DropTenantScope implements ChangeSource { private final ScopeCacheKey toDrop; private final List<CatalogEntity> drops; private final List<QueryStepOperation> dml; private final CacheInvalidationRecord record; public DropTenantScope(ScopeCacheKey scope) { toDrop = scope; drops = new ArrayList<CatalogEntity>(); dml = new ArrayList<QueryStepOperation>(); record = new CacheInvalidationRecord(scope.getTenantCacheKey(),InvalidationScope.CASCADE); } @Override public PETable getTable(SchemaContext sc) { return null; } @Override public List<QueryStepOperation> getDDLSteps() { return Collections.emptyList(); } @Override public List<QueryStepOperation> getDMLSteps() { return dml; } @Override public void beforeTxn(SSConnection ssCon) { drops.clear(); dml.clear(); } @Override public void inTxn(SchemaContext sc, CatalogDAO c) throws PEException { TableScope actualScope = sc.getSource().find(sc, toDrop); ExecutionSequence es = new ExecutionSequence(null); DeleteStatement ds = AdaptiveMultitenantSchemaPolicyContext.buildTenantDeleteFromTableStatement(sc, actualScope.getTable(sc), actualScope); ds.plan(sc,es, sc.getBehaviorConfiguration()); es.schedule(null, dml, null, sc, new IdentityConnectionValuesMap(sc.getValues()),null); sc.beginSaveContext(); try { drops.add(actualScope.getPersistent(sc)); } finally { sc.endSaveContext(); } Singletons.require(HostService.class).onGarbageEvent(); } @Override public CacheInvalidationRecord getInvalidationRecord() { return record; } @Override public List<CatalogEntity> getUpdates() { return Collections.emptyList(); } @Override public List<CatalogEntity> getDeletes() { return drops; } @Override public boolean requiresWorkers() { return true; } @Override public QueryStepOperation getCanonicalResultStep() { return null; } @Override public String describe() { return "DropTenantScope on " + toDrop; } @Override public boolean isWellFormed() { return false; } } public static class UpdateForeignKeyTargetTable implements ChangeSource { private final LazyAllocatedTable newTargetTable; private final UnqualifiedName revertToForward; private final TableCacheKey currentTable; private final UnqualifiedName constraintName; private final List<CatalogEntity> updates; private final List<QueryStepOperation> steps; private final CacheInvalidationRecord record; public UpdateForeignKeyTargetTable(TableCacheKey enclosing, UnqualifiedName constraintName, LazyAllocatedTable theNewTarget, CacheInvalidationRecord record) { this(enclosing,constraintName,theNewTarget,null,record); } public UpdateForeignKeyTargetTable(TableCacheKey enclosing, UnqualifiedName constraintName, UnqualifiedName revertToForward, CacheInvalidationRecord record) { this(enclosing,constraintName,null,revertToForward,record); } private UpdateForeignKeyTargetTable(TableCacheKey enclosing, UnqualifiedName constraintName, LazyAllocatedTable theNewTarget, UnqualifiedName revertToForward, CacheInvalidationRecord record) { this.newTargetTable = theNewTarget; this.revertToForward = revertToForward; this.updates = new ArrayList<CatalogEntity>(); this.steps = new ArrayList<QueryStepOperation>(); this.record = record; this.currentTable = enclosing; this.constraintName = constraintName; } @Override public PETable getTable(SchemaContext sc) { return newTargetTable.getTable(sc); } @Override public List<QueryStepOperation> getDDLSteps() { return steps; } @Override public List<QueryStepOperation> getDMLSteps() { return Collections.emptyList(); } @Override public void beforeTxn(SSConnection ssConn) { steps.clear(); updates.clear(); } @Override public void inTxn(SchemaContext sc, CatalogDAO c) throws PEException { PETable enclosing = sc.getSource().find(sc, currentTable).asTable(); sc.beginSaveContext(); try { enclosing.persistTree(sc); } finally { sc.endSaveContext(); } PEForeignKey constraint = null; for(PEKey pek : enclosing.getKeys(sc)) { if (!pek.isForeign()) continue; PEForeignKey pefk = (PEForeignKey) pek; if (pefk.getSymbol().equals(constraintName)) { constraint = pefk; break; } } TableKey yonKey = TableKey.make(sc,enclosing,sc.getNextTable()); AlterTableAction dropCurrentDefinition = new DropIndexAction(constraint); PEAlterTableStatement dropOld = new PEAlterTableStatement(sc, yonKey,dropCurrentDefinition); String dropCurrentSQL = dropOld.getSQL(sc); if (revertToForward == null) { PETable newTarget = newTargetTable.getTable(sc); for(PEKeyColumnBase pekc : constraint.getKeyColumns()) { PEForeignKeyColumn pefkc = (PEForeignKeyColumn) pekc; pefkc.setTargetColumn(newTarget.lookup(sc, pefkc.getTargetColumnName())); } constraint.setTargetTable(sc, newTarget); } else { constraint.revertToForwardMT(sc, revertToForward); } AlterTableAction addCurrentDefinition = new AddIndexAction(constraint); PEAlterTableStatement addNew = new PEAlterTableStatement(sc, yonKey,addCurrentDefinition); String addNewSQL = addNew.getSQL(sc); enclosing.setDeclaration(sc, enclosing); if (AdaptiveMTDDLPlannerUtils.isWellFormed(sc, enclosing, null) && enclosing.getState() == TableState.FIXED) { throw new IllegalStateException("Updated foreign key " + constraint.getSymbol() + ", table " + enclosing.getName() + " is now well formed but is not being flipped"); } sc.beginSaveContext(); try { enclosing.persistTree(sc); updates.addAll(sc.getSaveContext().getObjects()); } finally { sc.endSaveContext(); } Key k = constraint.getPersistent(sc); if (revertToForward == null) { UserTable nut = newTargetTable.getTable(sc).getPersistent(sc); k.setReferencedTable(nut); for(PEKeyColumnBase pekc : constraint.getKeyColumns()) { PEForeignKeyColumn pefkc = (PEForeignKeyColumn) pekc; KeyColumn kc = pefkc.getPersistent(sc); kc.setTargetColumn(nut.getUserColumn(pefkc.getTargetColumnName().getUnquotedName().get())); } } SessionExecutionStep drop = new SessionExecutionStep(enclosing.getDatabase(sc),enclosing.getStorageGroup(sc),dropCurrentSQL); SessionExecutionStep add = new SessionExecutionStep(enclosing.getDatabase(sc),enclosing.getStorageGroup(sc),addNewSQL); IdentityConnectionValuesMap cvm = new IdentityConnectionValuesMap(sc.getValues()); drop.schedule(null, steps, null, sc, cvm, null); add.schedule(null, steps, null, sc, cvm, null); } @Override public CacheInvalidationRecord getInvalidationRecord() { return record; } @Override public List<CatalogEntity> getUpdates() { return updates; } @Override public List<CatalogEntity> getDeletes() { return Collections.emptyList(); } @Override public boolean requiresWorkers() { return true; } @Override public QueryStepOperation getCanonicalResultStep() { return null; } @Override public String describe() { return "UpdateForeignKeyTargetTable on " + currentTable + ", symbol " + constraintName; } @Override public boolean isWellFormed() { if (newTargetTable != null) return newTargetTable.isWellFormed(); return false; } } public static class ExecuteAlters implements ChangeSource { private final PEAlterTableStatement original; private final LazyAllocatedTable target; private final List<CatalogEntity> updates; private final List<CatalogEntity> deletes; private final List<QueryStepOperation> ddl; private PETable finalDefinition; private final boolean isCanonical; private QueryStepOperation canonicalStep; public ExecuteAlters(PEAlterTableStatement src, boolean canonical, LazyAllocatedTable target) { this.original = src; this.target = target; this.updates = new ArrayList<CatalogEntity>(); this.deletes = new ArrayList<CatalogEntity>(); this.ddl = new ArrayList<QueryStepOperation>(); this.isCanonical = canonical; } @Override public PETable getTable(SchemaContext sc) { return finalDefinition; } @Override public List<QueryStepOperation> getDDLSteps() { return ddl; } @Override public List<QueryStepOperation> getDMLSteps() { return Collections.emptyList(); } @Override public void beforeTxn(SSConnection ssConn) { ddl.clear(); updates.clear(); deletes.clear(); } @Override public void inTxn(SchemaContext sc, CatalogDAO c) throws PEException { finalDefinition = target.getTable(sc); if (finalDefinition.getState() == TableState.SHARED) { throw new IllegalStateException("Altering shared table " + finalDefinition.getName()); } List<AlterTableAction> actions = original.getActions(); TableKey tk = TableKey.make(sc,finalDefinition,sc.getNextTable()); PEAlterTableStatement modded = new PEAlterTableStatement(sc,tk,actions); updates.addAll(modded.getCatalogEntries(sc)); deletes.addAll(modded.getDeleteObjects(sc)); SessionExecutionStep ses = new SessionExecutionStep(finalDefinition.getPEDatabase(sc),finalDefinition.getStorageGroup(sc), modded.getSQL(sc)); ses.schedule(null, ddl, null, sc, new IdentityConnectionValuesMap(sc.getValues()),null); if (isCanonical) canonicalStep = ddl.get(0); } @Override public CacheInvalidationRecord getInvalidationRecord() { return null; } @Override public List<CatalogEntity> getUpdates() { return updates; } @Override public List<CatalogEntity> getDeletes() { return deletes; } @Override public boolean requiresWorkers() { return true; } @Override public QueryStepOperation getCanonicalResultStep() { return canonicalStep; } @Override public String describe() { return "ExecuteAlters"; } @Override public boolean isWellFormed() { return target.isWellFormed(); } } // observe that once we do whatever we're supposed to do to // the nearest neighbors all the ddl acts the same - i.e. // currently well formed well formed with changes action // yes yes flip to shared // yes no flip to fixed // no yes flip to shared // no no alter in place public static class LateFKFixup { private final LazyAllocatedTable forwardToTable; private final Name revertToForwardTable; private final UnqualifiedName symbolName; private final String reason; public LateFKFixup(UnqualifiedName constraintSymbol, LazyAllocatedTable newTable, String reason) { this.symbolName = constraintSymbol; this.revertToForwardTable = null; this.forwardToTable = newTable; this.reason = reason; } public LateFKFixup(UnqualifiedName constraintSymbol, Name revertTo, String reason) { this.symbolName = constraintSymbol; this.revertToForwardTable = revertTo; this.forwardToTable = null; this.reason = reason; } public String getReason() { return reason; } public UnqualifiedName getSymbolName() { return symbolName; } public String getTargetName() { if (forwardToTable != null) return forwardToTable.describe(); return revertToForwardTable.get(); } public LazyAllocatedTable getTargetTable() { return forwardToTable; } public void fixupForeignKey(SchemaContext sc, PEForeignKey pefk) { if (revertToForwardTable != null) { pefk.revertToForwardMT(sc, revertToForwardTable); } else { pefk.setTargetTable(sc, forwardToTable.getTable(sc)); } } } // well formedness. the definition of a well formed table is: // [1] a table that has no foreign keys // [2] or a table that has no forward foreign keys and only references shared tables // // the other issue is we modify the definitions in memory - so when computing well formedness we need the ability // to pass in a mapping of old to new tables - and the new tables will then have a state associated with them. public static boolean isWellFormed(SchemaContext sc, PETable table, Map<UnqualifiedName, LateFKFixup> symbolForwarding) { for(PEKey pek : table.getKeys(sc)) { if (!pek.isForeign()) continue; PEForeignKey pefk = (PEForeignKey) pek; // first see if the symbol has been forwarded LateFKFixup fixed = (symbolForwarding == null ? null : symbolForwarding.get(pefk.getSymbol())); if (fixed != null) { if (fixed.getTargetTable() == null) { // been reverted to forward return false; } else if (!fixed.getTargetTable().isWellFormed()) { // been forwarded to something fixed return false; } else { continue; } } // hasn't been forwarded yet, use the local checks if (pefk.isForward()) return false; PETable targ = pefk.getTargetTable(sc); if (targ.getState() == TableState.FIXED) return false; } return true; } }