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.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import com.tesora.dve.common.LinkedHashSetFactory; import com.tesora.dve.common.MultiMap; import com.tesora.dve.common.catalog.TableState; import com.tesora.dve.sql.SchemaException; import com.tesora.dve.sql.ParserException.Pass; import com.tesora.dve.sql.schema.Name; 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.UnqualifiedName; 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.ChangeSource; import com.tesora.dve.sql.schema.mt.AdaptiveMTDDLPlannerUtils.CreateTableOperation; import com.tesora.dve.sql.schema.mt.AdaptiveMTDDLPlannerUtils.DirectAllocatedTable; import com.tesora.dve.sql.schema.mt.AdaptiveMTDDLPlannerUtils.LateFKFixup; import com.tesora.dve.sql.schema.mt.AdaptiveMTDDLPlannerUtils.LazyAllocatedTable; import com.tesora.dve.sql.schema.mt.AdaptiveMTDDLPlannerUtils.TableFlip; import com.tesora.dve.sql.schema.mt.AdaptiveMTDDLPlannerUtils.CompositeNestedOperation; import com.tesora.dve.sql.schema.mt.AdaptiveMTDDLPlannerUtils.UpdateForeignKeyTargetTable; 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; public abstract class FKRefMaintainer { protected final PETenant tenant; protected DependencyTree dt; protected ListOfPairs<TableScope, TaggedFK> rootSet; protected HashMap<PETable,ModificationBlock> modifications; protected ListSet<PETable> applicationOrder; protected HashMap<PETable, CreateTableOperation> forwarding; public FKRefMaintainer(PETenant onTenant) { this.tenant = onTenant; this.dt = null; this.modifications = new HashMap<PETable,ModificationBlock> (); this.applicationOrder = new ListSet<PETable>(); this.forwarding = new HashMap<PETable,CreateTableOperation>();; } // the first value is the target; the second is the fk that will be effected by that change. public abstract ListOfPairs<TableScope,TaggedFK> computeRootSet(SchemaContext sc); public abstract void modifyRoot(SchemaContext sc, Pair<TableScope, TaggedFK> r, Map<PETable,ModificationBlock> blocks); public DependencyTree getDepTree(SchemaContext sc) { if (dt == null) { dt = new DependencyTree(tenant); rootSet = computeRootSet(sc); for(Pair<TableScope, TaggedFK> p : rootSet) { dt.addDependency(sc, p.getSecond().getFK(), p.getSecond().getEnclosing(), p.getSecond().getScopeOfEnclosing(), p.getFirst().getTable(sc),p.getFirst()); } } return dt; } protected void createBlocks(SchemaContext sc) { for(PETable pet : dt.getAllTables()) { modifications.put(pet, new ModificationBlock(sc,dt.getScopeMapping().get(pet),pet)); } for(ModificationBlock mb : modifications.values()) { mb.buildInitialState(sc, modifications); } } public void maintain(SchemaContext sc) { // build the entire dep tree starting from the root set getDepTree(sc); createBlocks(sc); LinkedList<PETable> queue = new LinkedList<PETable>(); for(Pair<TableScope, TaggedFK> p : rootSet) { modifyRoot(sc,p,modifications); if (applicationOrder.add(p.getFirst().getTable(sc))) queue.add(p.getFirst().getTable(sc)); applicationOrder.add(p.getSecond().getEnclosing()); queue.add(p.getSecond().getEnclosing()); } while(!queue.isEmpty()) { PETable pet = queue.removeFirst(); applicationOrder.add(pet); ModificationBlock mb = modifications.get(pet); if (mb.propagateChanges(sc, modifications)) { // queue up all of my referrers Collection<TaggedFK> refs = dt.getReferring(mb.getSubject()); if (refs == null || refs.isEmpty()) continue; for(TaggedFK tfk : refs) queue.add(tfk.getEnclosing()); } } } public void schedule(SchemaContext sc, ExecutionSequence es, CompositeNestedOperation uno) { List<CreateTableOperation> nts = new ArrayList<CreateTableOperation>(); List<ChangeSource> cs = new ArrayList<ChangeSource>(); schedule(sc,nts,cs); for(CreateTableOperation cto : nts) { AdaptiveMTDDLPlannerUtils.addDDLCallback(sc, cto.getDefinition().getPEDatabase(sc), cto.getDefinition().getPersistentStorage(sc), cto.getDefinition(), Action.CREATE, es,cto, null); } uno.withChanges(cs); } public void schedule(SchemaContext sc, List<CreateTableOperation> newTabs, List<ChangeSource> changes) { // find all the tables that are different LinkedList<ModificationBlock> different = new LinkedList<ModificationBlock>(); for(ModificationBlock mb : modifications.values()) { if (mb.isFlipped()) different.add(mb); } // once the change order is set, we can flatten the blocks down to either a fk defs or a create table + a flip ListSet<ModificationBlock> changeOrder = new ListSet<ModificationBlock>(); while(!different.isEmpty()) { int before = different.size(); for(Iterator<ModificationBlock> iter = different.iterator(); iter.hasNext();) { ModificationBlock mb = iter.next(); // compute how the fk states have changed Map<PEForeignKey,FKState> mods = mb.getModified(); // this table can be scheduled if for all modifiable fk states the target tables have already been scheduled boolean ok = true; for(FKState fks : mods.values()) { if (fks instanceof ModifiableFKState) { ModifiableFKState mkfs = (ModifiableFKState) fks; if (mkfs.mb.isFlipped() && !changeOrder.contains(mkfs.mb)) { ok = false; break; } } } if (ok) { changeOrder.add(mb); iter.remove(); } } int after = different.size(); if (after == before) break; } if (!different.isEmpty()) { throw new SchemaException(Pass.PLANNER, "Cannot build unique decl order"); } // we can add everything else now - doesn't matter the order changeOrder.addAll(modifications.values()); for(ModificationBlock mb : changeOrder) { // if the table changed - use the old def + converting the fkmods to late resolving fks - build a cto // then schedule a flip. // if the table is not different - just schedule fkupdates List<LateFKFixup> fixups = new ArrayList<LateFKFixup>(); Map<PEForeignKey,FKState> mods = mb.getModified(); if (mods.isEmpty()) continue; for(Map.Entry<PEForeignKey, FKState> m : mods.entrySet()) { if (m.getValue() instanceof ForwardFKState) { // reverted to forward ForwardFKState f = (ForwardFKState) m.getValue(); fixups.add(new LateFKFixup(m.getKey().getSymbol(),f.targTab,"maintenance")); } else if (m.getValue() instanceof ModifiableFKState) { ModifiableFKState fkms = (ModifiableFKState) m.getValue(); LazyAllocatedTable lat = null; CreateTableOperation cto = forwarding.get(fkms.mb.getSubject()); if (cto != null) { lat = cto; } else { lat = new DirectAllocatedTable((TableCacheKey)fkms.mb.getSubject().getCacheKey(), fkms.mb.isWellFormed(sc, modifications)); } fixups.add(new LateFKFixup(m.getKey().getSymbol(),lat,"maintenance")); } else if (m.getValue() instanceof DirectFKState) { DirectFKState dfks = (DirectFKState) m.getValue(); LazyAllocatedTable lat = null; CreateTableOperation cto = forwarding.get(dfks.target); if (cto != null) lat = cto; else lat = new DirectAllocatedTable((TableCacheKey)dfks.target.getCacheKey(),AdaptiveMTDDLPlannerUtils.isWellFormed(sc, dfks.target, null)); fixups.add(new LateFKFixup(m.getKey().getSymbol(),lat,"maintenance")); } else { throw new SchemaException(Pass.PLANNER, "Unknown fk state kind " + m.getValue()); } } TableCacheKey subjectCacheKey = (TableCacheKey) mb.getSubject().getCacheKey(); if (mb.isFlipped()) { // new table Map<UnqualifiedName,LateFKFixup> mapped = new LinkedHashMap<UnqualifiedName,LateFKFixup>(); for(LateFKFixup f : fixups) { mapped.put(f.getSymbolName(),f); } CreateTableOperation cto = new CreateTableOperation(sc, mb.getSubject(), mb.scope.getName().getUnqualified(), tenant, (mb.isWellFormed(sc, modifications) ? TableState.SHARED : TableState.FIXED), mapped); forwarding.put(mb.getSubject(),cto); newTabs.add(cto); // also schedule a flip for it changes.add(new TableFlip(mb.scope, subjectCacheKey, cto, false, new CacheInvalidationRecord(mb.scope.getCacheKey(),InvalidationScope.LOCAL))); } else { // update any fks, but scheduling one fk update for each fixup if (mb.getSubject().getState() == TableState.SHARED) throw new IllegalStateException("Changing fk targets on shared table"); CacheInvalidationRecord record = new CacheInvalidationRecord(mb.scope.getCacheKey(),InvalidationScope.LOCAL); for(LateFKFixup f : fixups) { if (f.getTargetTable() == null) { changes.add(new UpdateForeignKeyTargetTable(subjectCacheKey, f.getSymbolName(), new UnqualifiedName(f.getTargetName()),record)); } else { changes.add(new UpdateForeignKeyTargetTable(subjectCacheKey, f.getSymbolName(), f.getTargetTable(), record)); } } } } } public static class DependencyTree { // reverse - key is table, value is fks referencing // every value in this is reachable from the original table (whether it was new or not) MultiMap<PETable,TaggedFK> deps; final PETenant tenant; // inverse mapping from PETable to TableScope for this tenant final HashMap<PETable,TableScope> scopes; public DependencyTree(PETenant onTenant) { deps = new MultiMap<PETable, TaggedFK>(new LinkedHashSetFactory<TaggedFK>()); this.tenant = onTenant; this.scopes = new HashMap<PETable,TableScope>(); } public Collection<TaggedFK> getReferring(PETable pet) { return deps.get(pet); } public Map<PETable,TableScope> getScopeMapping() { return scopes; } public Set<PETable> getAllTables() { LinkedHashSet<PETable> out = new LinkedHashSet<PETable>(); out.addAll(deps.keySet()); for(TaggedFK tfk : deps.values()) { out.add(tfk.getEnclosing()); } return out; } public void addDependency(SchemaContext sc, PEForeignKey pefk, PETable enc, TableScope encScope, PETable target, TableScope targScope) { // for the root set this will never short circuit, since the map is empty if (!deps.put(target, new TaggedFK(pefk,enc, encScope))) return; scopes.put(target,targScope); scopes.put(enc,encScope); // now process enc List<TableScope> yonScopes = sc.findScopesForFKTargets(enc, tenant); HashMap<PETable,TableScope> inverse = new HashMap<PETable,TableScope>(); ListOfPairs<PEForeignKey,PETable> toProcess = new ListOfPairs<PEForeignKey,PETable>(); for(TableScope ts : yonScopes) { PETable backing = ts.getTable(sc); inverse.put(backing,ts); for(PEKey pek : backing.getKeys(sc)) { if (!pek.isForeign()) continue; PEForeignKey ipefk = (PEForeignKey) pek; if (ipefk.isForward()) continue; PETable itarg = ipefk.getTargetTable(sc); if (itarg.getCacheKey().equals(enc.getCacheKey())) { toProcess.add(ipefk,backing); } } } for(Pair<PEForeignKey, PETable> p : toProcess) { addDependency(sc,p.getFirst(),p.getSecond(),inverse.get(p.getSecond()),enc, encScope); } } } public static class ModificationBlock { private static class StateEntry { private FKState state; private FKState orig; public StateEntry(FKState o) { state = o; orig = o; } public void update(FKState n) { state = n; } public FKState getState() { return state; } public boolean isModified() { return orig != state; } } private static class Version { private boolean wellFormed; private boolean flipped; public Version(boolean wf, boolean fl) { this.wellFormed = wf; this.flipped = fl; } } protected PETable subject; protected HashMap<PEForeignKey,StateEntry> refState; protected final TableScope scope; protected boolean flipped; protected Version state; public ModificationBlock(SchemaContext sc, TableScope onScope, PETable subj) { scope = onScope; subject = subj; refState = new HashMap<PEForeignKey,StateEntry>(); } public void buildInitialState(SchemaContext sc, Map<PETable, ModificationBlock> blocks) { boolean wellFormed = true; for(PEKey pek : subject.getKeys(sc)) { if (!pek.isForeign()) continue; PEForeignKey pefk = (PEForeignKey) pek; if (pefk.isForward()) { refState.put(pefk, new StateEntry(new ForwardFKState(pefk.getTargetTableName(sc)))); wellFormed = false; } else { PETable targ = pefk.getTargetTable(sc); boolean twf = AdaptiveMTDDLPlannerUtils.isWellFormed(sc, targ, null); if (!twf) wellFormed = false; ModificationBlock mb = blocks.get(targ); if (mb == null) { refState.put(pefk, new StateEntry(new DirectFKState(targ,twf))); } else { refState.put(pefk, new StateEntry(new ModifiableFKState(mb, twf))); } } } state = new Version(wellFormed,false); } public PETable getSubject() { return subject; } public void setFlipped() { flipped = true; } public boolean isFlipped() { return flipped; } public boolean propagateChanges(SchemaContext sc, Map<PETable,ModificationBlock> mods) { Version current = state; Version nstate = new Version(isWellFormed(sc,mods),flipped); boolean propagate = false; if (current.flipped != nstate.flipped) // we must propagate flips because dependents may need to be flipped as well propagate = true; if (current.wellFormed != nstate.wellFormed) { // if the table is shared and the formedness changed, we will need to flip // or if the table used to be not well formed (and fixed) but is now well formed (flip to shared) if (subject.getState() == TableState.SHARED || (subject.getState() == TableState.FIXED && nstate.wellFormed)) setFlipped(); propagate = true; } state = nstate; return propagate; } // modified if the state was modified, or if a modifiable target and it has been flipped public Map<PEForeignKey, FKState> getModified() { LinkedHashMap<PEForeignKey, FKState> out = new LinkedHashMap<PEForeignKey,FKState>(); for(Map.Entry<PEForeignKey, StateEntry> m : refState.entrySet()) { if (m.getValue().isModified()) out.put(m.getKey(),m.getValue().getState()); else if (m.getValue().getState() instanceof ModifiableFKState) { ModifiableFKState mkfs = (ModifiableFKState) m.getValue().getState(); if (mkfs.mb.isFlipped()) out.put(m.getKey(),mkfs); } } return out; } // all the mods I can do public void unresolveFK(PEForeignKey pefk, UnqualifiedName forwardName) { // see if I have existing state if (subject.getState() == TableState.SHARED) setFlipped(); StateEntry se = refState.get(pefk); FKState e = (se == null ? null : se.getState()); if (e == null || e instanceof ModifiableFKState) { ForwardFKState ns = new ForwardFKState(forwardName); if (se == null) { se = new StateEntry(ns); } else { se.update(ns); } refState.put(pefk, se); } else { se.update(new ForwardFKState(forwardName)); } } public void resolveFK(SchemaContext sc, Map<PETable,ModificationBlock> blocks, PEForeignKey pefk, ModificationBlock mb) { if (subject.getState() == TableState.SHARED) setFlipped(); StateEntry se = refState.get(pefk); FKState e = (se == null ? null : se.getState()); if (e == null || e instanceof ForwardFKState) { if (se == null) { se = new StateEntry(new ModifiableFKState(mb,mb.isWellFormed(sc, blocks))); } else { se.update(new ModifiableFKState(mb,mb.isWellFormed(sc, blocks))); } refState.put(pefk, se); } else { ModifiableFKState em = (ModifiableFKState) e; if (em.mb != mb) { se.update(new ModifiableFKState(mb,mb.isWellFormed(sc,blocks))); } } } public boolean isWellFormed(SchemaContext sc, Map<PETable, ModificationBlock> mods) { // guard against loops - check direct and forward first for(Map.Entry<PEForeignKey, StateEntry> m : refState.entrySet()) { if (m.getValue().getState() instanceof ModifiableFKState) continue; FKState fkm = m.getValue().getState(); if (fkm instanceof ForwardFKState) return false; if (fkm instanceof DirectFKState) { if (!fkm.wasWellFormed()) return false; } } // now do the others for(Map.Entry<PEForeignKey, StateEntry> m : refState.entrySet()) { if (!(m.getValue().getState() instanceof ModifiableFKState)) continue; ModifiableFKState mfks = (ModifiableFKState) m.getValue().getState(); if (mfks.modify(sc, mods)) { if (subject.getState() == TableState.SHARED) setFlipped(); } if (!mfks.wasWellFormed()) return false; } return true; } } // fk states protected static abstract class FKState { protected boolean lastWF; protected boolean lastFlipped; public FKState(boolean initWF) { lastWF = initWF; lastFlipped = false; } public abstract boolean modify(SchemaContext sc, Map<PETable,ModificationBlock> mods); public boolean wasWellFormed() { return lastWF; } public boolean wasFlipped() { return lastFlipped; } } // a table for which there is no mod block - it's state can never change protected static class DirectFKState extends FKState { protected PETable target; public DirectFKState(PETable targ, boolean wf) { super(wf); target = targ; } @Override public boolean modify(SchemaContext sc, Map<PETable, ModificationBlock> mods) { return false; } } // forward state - it might change protected static class ForwardFKState extends FKState { protected Name targTab; public ForwardFKState(Name unq) { super(false); targTab = unq; } @Override public boolean modify(SchemaContext sc, Map<PETable, ModificationBlock> mods) { return false; } } protected static class ModifiableFKState extends FKState { protected ModificationBlock mb; public ModifiableFKState(ModificationBlock targ, boolean wf) { super(wf); mb = targ; } @Override public boolean modify(SchemaContext sc, Map<PETable, ModificationBlock> mods) { boolean wf = mb.isWellFormed(sc, mods); boolean sig = false; if (lastWF != wf) { lastWF = wf; sig = true; } if (lastFlipped != mb.isFlipped()) { lastFlipped = mb.isFlipped(); sig = true; } return sig; } } public static class TaggedFK { protected final PEForeignKey pefk; protected final PETable enclosing; protected final TableScope scope; // scope associated with enclosing public TaggedFK(PEForeignKey pefk, PETable enc, TableScope ts) { this.pefk = pefk; this.enclosing = enc; this.scope = ts; } public PEForeignKey getFK() { return pefk; } public PETable getEnclosing() { return enclosing; } public TableScope getScopeOfEnclosing() { return scope; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((enclosing == null) ? 0 : enclosing.hashCode()); result = prime * result + ((pefk == null) ? 0 : pefk.hashCode()); result = prime * result + ((scope == null) ? 0 : scope.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; TaggedFK other = (TaggedFK) obj; if (enclosing == null) { if (other.enclosing != null) return false; } else if (!enclosing.equals(other.enclosing)) return false; if (pefk == null) { if (other.pefk != null) return false; } else if (!pefk.equals(other.pefk)) return false; if (scope == null) { if (other.scope != null) return false; } else if (!scope.equals(other.scope)) return false; return true; } } }