com.foundationdb.server.store.OnlineHelper.java Source code

Java tutorial

Introduction

Here is the source code for com.foundationdb.server.store.OnlineHelper.java

Source

/**
 * 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.store;

import com.foundationdb.ais.model.AkibanInformationSchema;
import com.foundationdb.ais.model.CacheValueGenerator;
import com.foundationdb.ais.model.Column;
import com.foundationdb.ais.model.Group;
import com.foundationdb.ais.model.GroupIndex;
import com.foundationdb.ais.model.Index;
import com.foundationdb.ais.model.Index.IndexType;
import com.foundationdb.ais.model.Table;
import com.foundationdb.ais.model.TableIndex;
import com.foundationdb.ais.util.TableChange.ChangeType;
import com.foundationdb.ais.util.TableChangeValidator.ChangeLevel;
import com.foundationdb.qp.exec.Plannable;
import com.foundationdb.qp.operator.API;
import com.foundationdb.qp.operator.ChainedCursor;
import com.foundationdb.qp.operator.Cursor;
import com.foundationdb.qp.operator.Operator;
import com.foundationdb.qp.operator.QueryBindings;
import com.foundationdb.qp.operator.QueryContext;
import com.foundationdb.qp.operator.Rebindable;
import com.foundationdb.qp.operator.SimpleQueryContext;
import com.foundationdb.qp.operator.StoreAdapter;
import com.foundationdb.qp.operator.Delete_Returning;
import com.foundationdb.qp.row.OverlayingRow;
import com.foundationdb.qp.row.ProjectedRow;
import com.foundationdb.qp.row.Row;
import com.foundationdb.qp.row.WriteIndexRow;
import com.foundationdb.qp.rowtype.ProjectedTableRowType;
import com.foundationdb.qp.rowtype.RowType;
import com.foundationdb.qp.rowtype.Schema;
import com.foundationdb.qp.rowtype.TableRowType;
import com.foundationdb.qp.storeadapter.indexrow.SpatialColumnHandler;
import com.foundationdb.qp.util.SchemaCache;
import com.foundationdb.server.error.ConcurrentViolationException;
import com.foundationdb.server.error.ConstraintViolationException;
import com.foundationdb.server.error.InvalidOperationException;
import com.foundationdb.server.error.NoSuchRowException;
import com.foundationdb.server.error.NotAllowedByConfigException;
import com.foundationdb.server.error.SQLParserInternalException;
import com.foundationdb.server.service.blob.BlobRef;
import com.foundationdb.server.types.aksql.aktypes.AkBlob;
import com.foundationdb.server.types.common.types.TypesTranslator;
import com.foundationdb.server.types.service.TypesRegistryService;
import com.foundationdb.server.service.dxl.DelegatingContext;
import com.foundationdb.server.service.listener.RowListener;
import com.foundationdb.server.service.session.Session;
import com.foundationdb.server.service.transaction.TransactionService;
import com.foundationdb.server.store.SchemaManager.OnlineChangeState;
import com.foundationdb.server.store.TableChanges.Change;
import com.foundationdb.server.store.TableChanges.ChangeSet;
import com.foundationdb.server.store.TableChanges.IndexChange;
import com.foundationdb.server.types.TCast;
import com.foundationdb.server.types.TInstance;
import com.foundationdb.server.types.texpressions.TCastExpression;
import com.foundationdb.server.types.texpressions.TPreparedExpression;
import com.foundationdb.server.types.texpressions.TPreparedField;
import com.foundationdb.server.types.value.ValueSource;
import com.foundationdb.sql.StandardException;
import com.foundationdb.sql.optimizer.CreateAsCompiler;
import com.foundationdb.sql.optimizer.plan.BasePlannable;
import com.foundationdb.sql.optimizer.rule.OperatorAssembler;
import com.foundationdb.sql.optimizer.rule.PlanContext;
import com.foundationdb.sql.optimizer.rule.PlanGenerator;
import com.foundationdb.ais.model.TableName;
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 com.google.common.collect.Multimap;
import com.persistit.Key;
import com.persistit.KeyState;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.UUID;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

public class OnlineHelper implements RowListener {
    private static final Logger LOG = LoggerFactory.getLogger(OnlineHelper.class);
    private static final Object TRANSFORM_CACHE_KEY = new Object();

    private final TransactionService txnService;
    private final SchemaManager schemaManager;
    private final Store store;
    private final TypesRegistryService typesRegistry;
    private final ConstraintHandler constraintHandler;
    private final boolean withConcurrentDML;

    public OnlineHelper(TransactionService txnService, SchemaManager schemaManager, Store store,
            TypesRegistryService typesRegistry, ConstraintHandler constraintHandler, boolean withConcurrentDML) {
        this.txnService = txnService;
        this.schemaManager = schemaManager;
        this.store = store;
        this.typesRegistry = typesRegistry;
        this.constraintHandler = constraintHandler;
        this.withConcurrentDML = withConcurrentDML;
    }

    public void buildIndexes(Session session, QueryContext context) {
        LOG.debug("Building indexes");
        txnService.beginTransaction(session);
        try {
            buildIndexesInternal(session, context);
            txnService.commitTransaction(session);
        } finally {
            txnService.rollbackTransactionIfOpen(session);
        }
    }

    public void checkTableConstraints(final Session session, QueryContext context) {
        LOG.debug("Checking constraints");
        txnService.beginTransaction(session);
        try {
            Collection<ChangeSet> changeSets = schemaManager.getOnlineChangeSets(session);
            assert (commonChangeLevel(changeSets) == ChangeLevel.METADATA_CONSTRAINT) : changeSets;
            // Gather all tables that need scanned, keyed by group
            AkibanInformationSchema oldAIS = schemaManager.getAis(session);
            Schema oldSchema = SchemaCache.globalSchema(oldAIS);
            Multimap<Group, RowType> groupMap = HashMultimap.create();
            for (ChangeSet cs : changeSets) {
                RowType rowType = oldSchema.tableRowType(cs.getTableId());
                groupMap.put(rowType.table().getGroup(), rowType);
            }
            // Scan all affected groups
            StoreAdapter adapter = store.createAdapter(session);
            final TransformCache transformCache = getTransformCache(session, null);
            for (Entry<Group, Collection<RowType>> entry : groupMap.asMap().entrySet()) {
                Operator plan = API.filter_Default(API.groupScan_Default(entry.getKey()), entry.getValue());
                runPlan(session, contextIfNull(context, adapter), schemaManager, txnService, plan,
                        new RowHandler() {
                            @Override
                            public void handleRow(Row row) {
                                simpleCheckConstraints(session, transformCache, row);
                            }
                        });
            }
        } finally {
            txnService.rollbackTransactionIfOpen(session);
        }
    }

    public void alterTable(Session session, QueryContext context) {
        LOG.debug("Altering table");
        txnService.beginTransaction(session);
        try {
            alterInternal(session, context);
        } finally {
            txnService.rollbackTransactionIfOpen(session);
        }
    }

    //
    // RowListener
    //

    public void onInsertPost(Session session, Table table, Key hKey, Row row) {
        TableTransform transform = getConcurrentDMLTransform(session, table);
        if (transform == null) {
            return;
        }
        try {
            concurrentDML(session, transform, hKey, null, row);
        } catch (ConstraintViolationException e) {
            setOnlineError(session, table, e);
        }

    }

    @Override
    public void onUpdatePre(Session session, Table table, Key hKey, Row oldRow, Row newRow) {
        TableTransform transform = getConcurrentDMLTransform(session, table);
        if (transform == null) {
            return;
        }
        try {
            concurrentDML(session, transform, hKey, oldRow, null);
        } catch (ConstraintViolationException e) {
            setOnlineError(session, table, e);
        }
    }

    @Override
    public void onUpdatePost(Session session, Table table, Key hKey, Row oldRow, Row newRow) {
        TableTransform transform = getConcurrentDMLTransform(session, table);
        if (transform == null) {
            return;
        }
        try {
            concurrentDML(session, transform, hKey, null, newRow);
        } catch (ConstraintViolationException e) {
            setOnlineError(session, table, e);
        }
    }

    @Override
    public void onDeletePre(Session session, Table table, Key hKey, Row row) {
        TableTransform transform = getConcurrentDMLTransform(session, table);
        if (transform == null) {
            return;
        }
        try {
            concurrentDML(session, transform, hKey, row, null);
        } catch (ConstraintViolationException e) {
            setOnlineError(session, table, e);
        }
    }

    //
    // ConstraintHandler.Handler-ish
    //

    public void handleInsert(Session session, Table table, Row row) {
        TableTransform transform = getConcurrentDMLTransform(session, table);
        if (transform == null) {
            return;
        }
        if (transform.checkConstraints) {
            boolean orig = txnService.setForceImmediateForeignKeyCheck(session, true);
            try {
                constraintHandler.handleInsert(session, transform.rowType.table(), row);
            } catch (ConstraintViolationException e) {
                setOnlineError(session, table, e);
            } finally {
                txnService.setForceImmediateForeignKeyCheck(session, orig);
            }
        }
    }

    public void handleUpdatePre(Session session, Table table, Row oldRow, Row newRow) {
        TableTransform transform = getConcurrentDMLTransform(session, table);
        if (transform == null) {
            return;
        }
        if (transform.checkConstraints) {
            boolean orig = txnService.setForceImmediateForeignKeyCheck(session, true);
            try {
                constraintHandler.handleUpdatePre(session, transform.rowType.table(), oldRow, newRow);
            } catch (ConstraintViolationException e) {
                setOnlineError(session, table, e);
            } finally {
                txnService.setForceImmediateForeignKeyCheck(session, orig);
            }
        }
    }

    public void handleUpdatePost(Session session, Table table, Row oldRow, Row newRow) {
        TableTransform transform = getConcurrentDMLTransform(session, table);
        if (transform == null) {
            return;
        }
        if (transform.checkConstraints) {
            boolean orig = txnService.setForceImmediateForeignKeyCheck(session, true);
            try {
                constraintHandler.handleUpdatePost(session, transform.rowType.table(), oldRow, newRow);
            } catch (ConstraintViolationException e) {
                setOnlineError(session, table, e);
            } finally {
                txnService.setForceImmediateForeignKeyCheck(session, orig);
            }
        }
    }

    public void handleDelete(Session session, Table table, Row row) {
        TableTransform transform = getConcurrentDMLTransform(session, table);
        if (transform == null) {
            return;
        }
        if (transform.checkConstraints) {
            boolean orig = txnService.setForceImmediateForeignKeyCheck(session, true);
            try {
                constraintHandler.handleDelete(session, transform.rowType.table(), row);
            } catch (ConstraintViolationException e) {
                setOnlineError(session, table, e);
            } finally {
                txnService.setForceImmediateForeignKeyCheck(session, orig);
            }
        }
    }

    public void handleTruncate(Session session, Table table) {
        TableTransform transform = getConcurrentDMLTransform(session, table);
        if (transform == null) {
            return;
        }
        if (transform.checkConstraints) {
            boolean orig = txnService.setForceImmediateForeignKeyCheck(session, true);
            try {
                constraintHandler.handleTruncate(session, transform.rowType.table());
            } catch (ConstraintViolationException e) {
                setOnlineError(session, table, e);
            } finally {
                txnService.setForceImmediateForeignKeyCheck(session, orig);
            }
        }
    }

    //
    // Internal
    //

    private void setOnlineError(Session session, Table t, ConstraintViolationException e) {
        // Note: Written in the same transaction executing DML, checked in session executing DDL
        schemaManager.setOnlineDMLError(session, t.getTableId(), e.getMessage());
    }

    private void buildIndexesInternal(Session session, QueryContext context) {
        Collection<ChangeSet> changeSets = schemaManager.getOnlineChangeSets(session);
        ChangeLevel changeLevel = commonChangeLevel(changeSets);
        assert (changeLevel == ChangeLevel.INDEX || changeLevel == ChangeLevel.INDEX_CONSTRAINT) : changeSets;
        TransformCache transformCache = getTransformCache(session, null);
        Multimap<Group, RowType> tableIndexes = HashMultimap.create();
        Set<GroupIndex> groupIndexes = new HashSet<>();
        for (ChangeSet cs : changeSets) {
            TableTransform transform = transformCache.get(cs.getTableId());
            tableIndexes.put(transform.rowType.table().getGroup(), transform.rowType);
            groupIndexes.addAll(transform.groupIndexes);
        }

        StoreAdapter adapter = store.createAdapter(session);
        if (!tableIndexes.isEmpty()) {
            buildTableIndexes(session, context, adapter, transformCache, tableIndexes);
        }
        if (!groupIndexes.isEmpty()) {
            if (changeLevel == ChangeLevel.INDEX_CONSTRAINT) {
                throw new IllegalStateException("Constraint and group indexes");
            }
            buildGroupIndexes(session, context, adapter, groupIndexes);
        }
    }

    public void createAsSelect(final Session session, QueryContext context, final ServerSession server,
            String queryExpression, TableName tableName) {
        LOG.debug("Creating Table As Select Online");

        txnService.beginTransaction(session);
        try {
            SQLParser parser = server.getParser();
            StatementNode stmt;
            String statement = "insert into " + tableName.toStringEscaped() + " " + queryExpression;
            try {
                stmt = parser.parseStatement(statement);
            } catch (StandardException e) {
                throw new SQLParserInternalException(e);//make specific runtime error unexpectedException
            }
            AkibanInformationSchema onlineAIS = schemaManager.getOnlineAIS(session);
            StoreAdapter adapter = store.createAdapter(session);
            CreateAsCompiler compiler = new CreateAsCompiler(server, adapter, false, onlineAIS);
            DMLStatementNode dmlStmt = (DMLStatementNode) stmt;
            PlanContext planContext = new PlanContext(compiler);

            BasePlannable result = compiler.compile(dmlStmt, null, planContext);

            Plannable plannable = result.getPlannable();
            QueryContext newContext = contextIfNull(context, adapter);
            getTransformCache(session, server);
            runPlan(session, newContext, schemaManager, txnService, (Operator) plannable, null);
        } finally {
            txnService.commitTransaction(session);
        }
    }

    private void alterInternal(final Session session, QueryContext context) {
        final Collection<ChangeSet> changeSets = schemaManager.getOnlineChangeSets(session);
        final ChangeLevel changeLevel = commonChangeLevel(changeSets);
        assert (changeLevel == ChangeLevel.TABLE || changeLevel == ChangeLevel.GROUP) : changeSets;

        final AkibanInformationSchema origAIS = schemaManager.getAis(session);
        final AkibanInformationSchema newAIS = schemaManager.getOnlineAIS(session);

        final StoreAdapter origAdapter = store.createAdapter(session);
        final QueryContext origContext = new DelegatingContext(origAdapter, context);
        final QueryBindings origBindings = origContext.createBindings();

        final TransformCache transformCache = getTransformCache(session, null);
        Set<Table> origRoots = findOldRoots(changeSets, origAIS, newAIS);

        for (Table root : origRoots) {
            final LobCheck lobCheckRes = checkForDropLob(root, changeSets, origAIS);

            Operator plan = API.groupScan_Default(root.getGroup());
            runPlan(session, contextIfNull(context, origAdapter), schemaManager, txnService, plan,
                    new RowHandler() {
                        @Override
                        public void handleRow(Row oldRow) {
                            TableTransform transform = transformCache.get(oldRow.rowType().typeId());
                            Row newRow = transformRow(origContext, origBindings, transform, oldRow);
                            if (lobCheckRes != null) {
                                registerLob(session, oldRow, lobCheckRes.tableId, lobCheckRes.columnPos);
                            }
                            origAdapter.writeRow(newRow, transform.tableIndexes, transform.groupIndexes);
                        }
                    });
        }
    }

    private LobCheck checkForDropLob(Table root, Collection<ChangeSet> changeSets, AkibanInformationSchema oldAIS) {
        LobCheck lc = null;
        for (ChangeSet cs : changeSets) {
            if (cs.hasChangeLevel() && cs.getChangeLevel().equals(ChangeLevel.TABLE.name())) {
                Table oldTable = oldAIS.getTable(cs.getOldSchema(), cs.getOldName());
                if (oldTable.getGroup().getRoot() == root) {
                    for (int i = 0; i < cs.getColumnChangeCount(); i++) {
                        Change change = cs.getColumnChange(i);
                        if (change.hasChangeType() && change.getChangeType().equals("DROP")) {
                            String oldColName = change.getOldName();
                            Column col = oldTable.getColumn(oldColName);
                            if (AkBlob.isBlob(col.getType().typeClass())) {
                                assert lc == null; // change set can only contain a single column drop
                                lc = new LobCheck();
                                lc.tableId = oldTable.getTableId();
                                lc.columnPos = col.getPosition();
                            }
                        }
                    }
                }
            }
        }
        return lc;
    }

    private class LobCheck {
        public int tableId;
        public int columnPos;
    }

    private void registerLob(Session session, Row oldRow, int tableId, int dropField) {
        if (oldRow.rowType().typeId() == tableId) {
            ValueSource val = oldRow.value(dropField);
            Object blob = val.getObject();
            if (blob instanceof BlobRef) {
                if (((BlobRef) blob).isLongLob()) {
                    UUID blobId = ((BlobRef) blob).getId();
                    if (store instanceof FDBStore) {
                        TableName rootTable = oldRow.rowType().table().getGroup().getName();
                        ((FDBStore) store).registerLobForOnlineDelete(session, rootTable, blobId);
                    }
                }
            }
        }
    }

    private void buildTableIndexes(final Session session, QueryContext context, StoreAdapter adapter,
            final TransformCache transformCache, Multimap<Group, RowType> tableIndexes) {
        final WriteIndexRow buffer = new WriteIndexRow();
        for (Entry<Group, Collection<RowType>> entry : tableIndexes.asMap().entrySet()) {
            if (entry.getValue().isEmpty()) {
                continue;
            }
            Operator plan = API.filter_Default(API.groupScan_Default(entry.getKey()), entry.getValue());
            runPlan(session, contextIfNull(context, adapter), schemaManager, txnService, plan, new RowHandler() {
                @Override
                public void handleRow(final Row row) {
                    TableTransform transform = transformCache.get(row.rowType().typeId());
                    simpleCheckConstraints(session, transformCache, row);
                    for (final TableIndex index : transform.tableIndexes) {
                        final Key hKey = store.createKey();
                        row.hKey().copyTo(hKey);
                        if (index.isSpatial()) {
                            final SpatialColumnHandler spatialColumnHandler = new SpatialColumnHandler(index);
                            spatialColumnHandler.processSpatialObject(row, new SpatialColumnHandler.Operation() {
                                @Override
                                public void handleZValue(long z) {
                                    store.writeIndexRow(session, index, row, hKey, buffer, spatialColumnHandler, z,
                                            true);
                                }
                            });
                        } else {
                            store.writeIndexRow(session, index, row, hKey, buffer, null, -1L, true);
                        }
                    }
                }
            });
        }
    }

    @SuppressWarnings("unchecked")
    private void buildGroupIndexes(final Session session, QueryContext context, StoreAdapter adapter,
            Collection<GroupIndex> groupIndexes) {
        if (groupIndexes.isEmpty()) {
            return;
        }
        for (final GroupIndex groupIndex : groupIndexes) {
            Schema schema = SchemaCache.globalSchema(groupIndex.getAIS());
            final Operator plan = StoreGIMaintenancePlans.groupIndexCreationPlan(schema, groupIndex);
            final StoreGIHandler giHandler = StoreGIHandler.forBuilding((AbstractStore) store, session, schema,
                    groupIndex);
            runPlan(session, contextIfNull(context, adapter), schemaManager, txnService, plan, new RowHandler() {
                @Override
                public void handleRow(Row row) {
                    giHandler.handleRow(groupIndex, row, StoreGIHandler.Action.STORE);
                }
            });
        }
    }

    private void simpleCheckConstraints(Session session, TransformCache transformCache, Row row) {
        TableTransform transform = transformCache.get(row.rowType().typeId());
        if (transform == null || !transform.checkConstraints) {
            return;
        }
        constraintHandler.handleInsert(session, transform.rowType.table(), row);
    }

    private void concurrentDML(final Session session, final TableTransform transform, final Key hKey,
            final Row oldRow, final Row newRow) {
        final boolean doDelete = (oldRow != null);
        final boolean doWrite = (newRow != null);
        QueryContext context = null;
        switch (transform.changeLevel) {
        case INDEX:
            if (!transform.tableIndexes.isEmpty()) {
                final WriteIndexRow buffer = new WriteIndexRow();
                for (final TableIndex index : transform.tableIndexes) {
                    long oldZValue = -1;
                    long newZValue = -1;
                    if (index.isSpatial()) {
                        final SpatialColumnHandler spatialColumnHandler = new SpatialColumnHandler(index);
                        if (doDelete) {
                            spatialColumnHandler.processSpatialObject(oldRow, new SpatialColumnHandler.Operation() {
                                @Override
                                public void handleZValue(long z) {
                                    store.deleteIndexRow(session, index, oldRow, hKey, buffer, spatialColumnHandler,
                                            z, false);
                                }
                            });
                        }
                        if (doWrite) {
                            spatialColumnHandler.processSpatialObject(oldRow, new SpatialColumnHandler.Operation() {
                                @Override
                                public void handleZValue(long z) {
                                    Row outputRow = new OverlayingRow(newRow, transform.rowType);
                                    store.writeIndexRow(session, index, outputRow, hKey, buffer,
                                            spatialColumnHandler, z, false);
                                }
                            });
                        }
                    } else {
                        if (doDelete) {
                            store.deleteIndexRow(session, index, oldRow, hKey, buffer, null, -1L, false);
                        }
                        if (doWrite) {
                            Row outputRow = new OverlayingRow(newRow, transform.rowType);
                            store.writeIndexRow(session, index, outputRow, hKey, buffer, null, -1L, false);
                        }
                    }
                }
            }
            if (!transform.groupIndexes.isEmpty()) {
                if (doDelete) {
                    Row deleteRow = new OverlayingRow(oldRow, transform.rowType);
                    store.deleteIndexRows(session, transform.rowType.table(), deleteRow, transform.groupIndexes);
                }
                if (doWrite) {
                    Row outputRow = new OverlayingRow(newRow, transform.rowType);
                    store.writeIndexRows(session, transform.rowType.table(), outputRow, transform.groupIndexes);
                }
            }
            break;
        case TABLE:
            if (transform.deleteOperator != null && transform.insertOperator != null) {
                StoreAdapter adapter = store.createAdapter(session);
                context = new SimpleQueryContext(adapter);
                QueryBindings bindings = context.createBindings();
                if (doDelete) {
                    bindings.setRow(OperatorAssembler.CREATE_AS_BINDING_POSITION, oldRow);
                    try {
                        runPlan(context, transform.deleteOperator, bindings);
                    } catch (NoSuchRowException e) {
                        LOG.debug("row not present: {}", oldRow);
                    }
                }
                if (doWrite) {

                    bindings.setRow(OperatorAssembler.CREATE_AS_BINDING_POSITION,
                            transformRow(context, bindings, transform, newRow));
                    try {
                        runPlan(context, transform.insertOperator, bindings);
                    } catch (NoSuchRowException e) {
                        LOG.debug("row not present: {}", newRow);
                    }
                }
                break;
            }
        case GROUP:
            StoreAdapter adapter = store.createAdapter(session);
            context = new SimpleQueryContext(adapter);
            QueryBindings bindings = context.createBindings();
            if (doDelete) {
                Row newOldRow = transformRow(context, bindings, transform, oldRow);
                try {
                    adapter.deleteRow(newOldRow, false);
                } catch (NoSuchRowException e) {
                    LOG.debug("row not present: {}", newOldRow);
                }
            }
            if (doWrite) {
                Row newNewRow = transformRow(context, bindings, transform, newRow);
                adapter.writeRow(newNewRow, transform.tableIndexes, transform.groupIndexes);
            }
            break;
        }
        transform.hKeySaver.save(schemaManager, session, hKey);
    }

    private TransformCache getTransformCache(final Session session, final ServerSession server) {
        AkibanInformationSchema ais = schemaManager.getAis(session);
        TransformCache cache = ais.getCachedValue(TRANSFORM_CACHE_KEY, null);
        if (cache == null) {
            cache = ais.getCachedValue(TRANSFORM_CACHE_KEY, new CacheValueGenerator<TransformCache>() {
                @Override
                public TransformCache valueFor(AkibanInformationSchema ais) {
                    TransformCache cache = new TransformCache();
                    TypesTranslator typesTranslator = schemaManager.getTypesTranslator();
                    Collection<OnlineChangeState> states = schemaManager.getOnlineChangeStates(session);
                    for (OnlineChangeState s : states) {
                        buildTransformCache(cache, s.getChangeSets(), ais, s.getAIS(), typesRegistry,
                                typesTranslator, session, server, store);
                    }
                    return cache;
                }
            });
        }
        return cache;
    }

    private TableTransform getConcurrentDMLTransform(Session session, Table table) {
        if (!schemaManager.isOnlineActive(session, table.getTableId())) {
            return null;
        }
        if (!withConcurrentDML) {
            throw new NotAllowedByConfigException("DML during online DDL");
        }
        TableTransform transform = getTransformCache(session, null).get(table.getTableId());
        if (isTransformedTable(transform, table)) {
            return null;
        }
        return transform;
    }

    //
    // Static
    //

    private static void buildTransformCache(TransformCache cache, Collection<ChangeSet> changeSets,
            AkibanInformationSchema oldAIS, AkibanInformationSchema newAIS, TypesRegistryService typesRegistry,
            TypesTranslator typesTranslator, Session session, ServerSession server, Store givenStore) {

        final ChangeLevel changeLevel = commonChangeLevel(changeSets);
        final Schema newSchema = SchemaCache.globalSchema(newAIS);
        Plannable deletePlan = null;
        Plannable insertPlan = null;
        for (ChangeSet cs : changeSets) {
            if (cs.hasSelectStatement()) {
                SQLParser parser = server.getParser();
                StatementNode insertStmt;
                try {
                    insertStmt = parser.parseStatement(
                            "insert into " + newAIS.getTable(cs.getToTableId()).getName().toStringEscaped() + " "
                                    + cs.getSelectStatement());
                } catch (StandardException e) {
                    throw new SQLParserInternalException(e);
                }
                StoreAdapter adapter = givenStore.createAdapter(session);
                CreateAsCompiler compiler = new CreateAsCompiler(server, adapter, true, newAIS);
                PlanContext planContext = new PlanContext(compiler);
                BasePlannable insertResult = compiler.compile((DMLStatementNode) insertStmt, null, planContext);
                insertPlan = insertResult.getPlannable();
                deletePlan = new Delete_Returning(insertPlan.getInputOperators().iterator().next(), false);
            }
            int tableID = cs.getTableId();
            TableRowType newType = newSchema.tableRowType(tableID);
            TableTransform transform = buildTableTransform(cs, changeLevel, oldAIS, newType, typesRegistry,
                    typesTranslator, (Operator) deletePlan, (Operator) insertPlan);
            TableTransform prev = cache.put(tableID, transform);
            assert (prev == null) : tableID;
        }
    }

    private static void runPlan(Session session, QueryContext context, SchemaManager schemaManager,
            TransactionService txnService, Operator plan, RowHandler handler) {
        LOG.debug("Running online plan: {}", plan);
        Map<RowType, HKeyChecker> checkers = new HashMap<>();
        QueryBindings bindings = context.createBindings();
        Cursor cursor = API.cursor(plan, context, bindings);
        Rebindable rebindable = getRebindable(cursor);
        cursor.openTopLevel();
        try {
            boolean done = false;
            Row lastCommitted = null;
            boolean checkOnlineError = true;
            long rowCount = 0;
            while (!done) {
                Row row = cursor.next();
                boolean didCommit = false;
                boolean didRollback = false;
                if (checkOnlineError) {
                    // Checked once per transaction here and in final phase in DDLFunctions
                    checkOnlineError(session, schemaManager);
                    checkOnlineError = false;
                }
                if (row != null) {
                    rowCount++;
                    RowType rowType = row.rowType();
                    // No way to pre-populate this map as Operator#rowType() is optional and insufficient.
                    HKeyChecker checker = checkers.get(rowType);
                    if (checker == null) {
                        if (rowType.hasTable()) {
                            checker = new SchemaManagerChecker(rowType.table().getTableId());
                        } else {
                            checker = new FalseChecker();
                        }
                        checkers.put(row.rowType(), checker);
                    }
                    try {
                        if (handler != null) {
                            //TODO: Not correct but only option for createAs due to hidden PK
                            Key hKey = new Key(null, 2047);
                            row.hKey().copyTo(hKey);
                            if (!checker.contains(schemaManager, session, hKey)) {
                                handler.handleRow(row);
                            } else {
                                LOG.trace("skipped row: {}", row);
                            }
                        }
                        didCommit = txnService.periodicallyCommit(session);
                    } catch (InvalidOperationException e) {
                        if (!e.getCode().isRollbackClass()) {
                            throw e;
                        }
                        didRollback = true;
                    }
                } else {
                    // Cursor exhausted, completely finished
                    didRollback = txnService.commitOrRetryTransaction(session);
                    done = didCommit = !didRollback;
                    if (didCommit) {
                        txnService.beginTransaction(session);
                    }
                }
                if (didCommit) {
                    LOG.debug("Committed up to row: {}: {} rows", row, rowCount);
                    checkOnlineError = true;
                    lastCommitted = row;
                    checkers.clear();
                } else if (didRollback) {
                    LOG.debug("Rolling back to row: {}", lastCommitted);
                    checkOnlineError = true;
                    checkers.clear();
                    txnService.rollbackTransactionIfOpen(session);
                    txnService.beginTransaction(session);
                    cursor.closeTopLevel();
                    rebindable.rebind((lastCommitted == null) ? null : lastCommitted.hKey(), true);
                    cursor.openTopLevel();
                }
            }
        } finally {
            cursor.closeTopLevel();
        }
    }

    private static void runPlan(QueryContext context, Operator plan, QueryBindings bindings) {
        LOG.debug("Running online DML plan: {}", plan);
        Map<RowType, HKeyChecker> checkers = new HashMap<>();
        Cursor cursor = API.cursor(plan, context, bindings);
        cursor.openTopLevel();//open up top cursor
        try {
            boolean done = false;
            while (!done) {
                Row row = cursor.next();
                if (row != null) {
                    RowType rowType = row.rowType();
                    HKeyChecker checker = checkers.get(rowType);
                    if (checker == null) {
                        if (rowType.hasTable()) {
                            checker = new SchemaManagerChecker(rowType.table().getTableId());
                        } else {
                            checker = new FalseChecker();
                        }
                        checkers.put(row.rowType(), checker);
                    }
                } else {
                    done = true;
                }
            }
        } finally {
            cursor.closeTopLevel();
        }
    }

    private static Set<Table> findOldRoots(Collection<ChangeSet> changeSets, AkibanInformationSchema oldAIS,
            AkibanInformationSchema newAIS) {
        Set<Table> oldRoots = new HashSet<>();
        for (ChangeSet cs : changeSets) {
            Table oldTable = oldAIS.getTable(cs.getTableId());
            Table newTable = newAIS.getTable(cs.getTableId());
            Table oldNewTable = oldAIS.getTable(newTable.getTableId());
            oldRoots.add(oldTable.getGroup().getRoot());
            oldRoots.add(oldNewTable.getGroup().getRoot());
        }
        return oldRoots;
    }

    private static void checkOnlineError(Session session, SchemaManager sm) {
        String msg = sm.getOnlineDMLError(session);
        if (msg != null) {
            throw new ConcurrentViolationException(msg);
        }
    }

    /** Find all {@code ADD} or {@code MODIFY} group indexes referenced by {@code changeSets}. */
    public static Collection<Index> findIndexesToBuild(Collection<ChangeSet> changeSets,
            AkibanInformationSchema ais) {
        // There may be duplicates (e.g. every table has a GI it participates in)
        Collection<Index> newIndexes = new HashSet<>();
        for (ChangeSet cs : changeSets) {
            Table table = ais.getTable(cs.getTableId());
            for (IndexChange ic : cs.getIndexChangeList()) {
                ChangeType changeType = ChangeType.valueOf(ic.getChange().getChangeType());
                if (changeType == ChangeType.ADD || changeType == ChangeType.MODIFY) {
                    String name = ic.getChange().getNewName();
                    final Index index;
                    switch (IndexType.valueOf(ic.getIndexType())) {
                    case TABLE:
                        index = table.getIndexIncludingInternal(name);
                        break;
                    case FULL_TEXT:
                        index = table.getFullTextIndex(name);
                        break;
                    case GROUP:
                        index = table.getGroup().getIndex(name);
                        break;
                    default:
                        throw new IllegalStateException(ic.getIndexType());
                    }
                    assert index != null : ic;
                    newIndexes.add(index);
                }
            }
        }
        return newIndexes;
    }

    /** Find all {@code ADD} or {@code MODIFY} table indexes from {@code changeSet}. */
    private static Collection<TableIndex> findTableIndexesToBuild(ChangeSet changeSet, Table newTable) {
        if (changeSet == null) {
            return Collections.emptyList();
        }
        List<TableIndex> tableIndexes = new ArrayList<>();
        for (IndexChange ic : changeSet.getIndexChangeList()) {
            if (IndexType.TABLE.name().equals(ic.getIndexType())) {
                switch (ChangeType.valueOf(ic.getChange().getChangeType())) {
                case ADD:
                case MODIFY:
                    TableIndex index = newTable.getIndexIncludingInternal(ic.getChange().getNewName());
                    assert (index != null) : newTable.toString() + "," + ic;
                    tableIndexes.add(index);
                    break;
                }
            }
        }
        return tableIndexes;
    }

    /** Find all {@code ADD} or {@code MODIFY} group indexes from {@code changeSet}. */
    private static Collection<GroupIndex> findGroupIndexesToBuild(ChangeSet changeSet, Table newTable) {
        if (changeSet == null) {
            return Collections.emptyList();
        }
        List<GroupIndex> groupIndexes = new ArrayList<>();
        Group group = newTable.getGroup();
        for (IndexChange ic : changeSet.getIndexChangeList()) {
            if (IndexType.GROUP.name().equals(ic.getIndexType())) {
                switch (ChangeType.valueOf(ic.getChange().getChangeType())) {
                case ADD:
                case MODIFY:
                    GroupIndex index = group.getIndex(ic.getChange().getNewName());
                    assert index != null : ic;
                    groupIndexes.add(index);
                    break;
                }
            }
        }
        return groupIndexes;
    }

    /** Find {@code newColumn}'s position in {@code oldTable} or {@code null} if it wasn't present */
    private static Integer findOldPosition(List<Change> columnChanges, Table oldTable, Column newColumn) {
        String newName = newColumn.getName();
        for (Change change : columnChanges) {
            if (newName.equals(change.getNewName())) {
                switch (ChangeType.valueOf(change.getChangeType())) {
                case ADD:
                    return null;
                case MODIFY:
                    Column oldColumn = oldTable.getColumn(change.getOldName());
                    assert oldColumn != null : newColumn;
                    return oldColumn.getPosition();
                case DROP:
                    throw new IllegalStateException("Dropped new column: " + newName);
                }
            }
        }
        Column oldColumn = oldTable.getColumn(newName);
        if ((oldColumn == null) && newColumn.isAkibanPKColumn()) {
            return null;
        }
        // Not in change list, must be an original column
        assert oldColumn != null : newColumn;
        return oldColumn.getPosition();
    }

    private static ProjectedTableRowType buildProjectedRowType(ChangeSet changeSet, Table origTable,
            RowType newRowType, boolean isGroupChange, TypesRegistryService typesRegistry,
            TypesTranslator typesTranslator, QueryContext origContext) {
        Table newTable = newRowType.table();
        final List<Column> newColumns = newTable.getColumnsIncludingInternal();
        final List<TPreparedExpression> projections = new ArrayList<>(newColumns.size());
        for (Column newCol : newColumns) {
            Integer oldPosition = findOldPosition(changeSet.getColumnChangeList(), origTable, newCol);
            TInstance newInst = newCol.getType();
            if (oldPosition == null) {
                projections.add(buildColumnDefault(newCol, typesRegistry, typesTranslator, origContext));
            } else {
                Column oldCol = origTable.getColumnsIncludingInternal().get(oldPosition);
                TInstance oldInst = oldCol.getType();
                TPreparedExpression pExp = new TPreparedField(oldInst, oldPosition);
                if (!oldInst.equalsExcludingNullable(newInst)) {
                    TCast cast = typesRegistry.getCastsResolver().cast(oldInst.typeClass(), newInst.typeClass());
                    pExp = new TCastExpression(pExp, cast, newInst);
                }
                projections.add(pExp);
            }
        }
        return new ProjectedTableRowType(newRowType.schema(), newTable, projections, true);
    }

    // This should be quite similar to ExpressionAssembler#assembleColumnDefault()
    private static TPreparedExpression buildColumnDefault(Column newCol, TypesRegistryService typesRegistry,
            TypesTranslator typesTranslator, QueryContext origContext) {
        return PlanGenerator.generateDefaultExpression(newCol, null, typesRegistry, typesTranslator, origContext);
    }

    private static TableTransform buildTableTransform(ChangeSet changeSet, ChangeLevel changeLevel,
            AkibanInformationSchema oldAIS, TableRowType newRowType, TypesRegistryService typesRegistry,
            TypesTranslator typesTranslator, Operator deleteOperator, Operator insertOperator) {
        Table newTable = newRowType.table();
        Collection<TableIndex> tableIndexes = findTableIndexesToBuild(changeSet, newTable);
        Collection<GroupIndex> groupIndexes = findGroupIndexesToBuild(changeSet, newTable);
        ProjectedTableRowType projectedRowType = null;
        boolean checkConstraints = false;
        switch (changeLevel) {
        case METADATA_CONSTRAINT:
        case INDEX_CONSTRAINT:
            checkConstraints = true;
            assert groupIndexes.isEmpty() : groupIndexes;
            break;
        case TABLE:
            if (deleteOperator != null && insertOperator != null)
                break;
        case GROUP:
            Table oldTable = oldAIS.getTable(newTable.getTableId());
            if ((changeSet.getColumnChangeCount() > 0)
                    || (newRowType.nFields() != oldTable.getColumnsIncludingInternal().size())) {
                projectedRowType = buildProjectedRowType(changeSet, oldTable, newRowType,
                        changeLevel == ChangeLevel.GROUP, typesRegistry, typesTranslator, new SimpleQueryContext());
            }
            break;
        }
        return new TableTransform(changeLevel, new SchemaManagerSaver(changeSet.getTableId()), newRowType,
                projectedRowType, checkConstraints, tableIndexes, groupIndexes, deleteOperator, insertOperator);
    }

    /**
     * NB: Current usage is *only* with plans that have GroupScan at the bottom. Use this fact to find the bottom,
     * which can rebind(), for when periodicCommit() fails.
     */
    private static Rebindable getRebindable(Cursor cursor) {
        Cursor toRebind = cursor;
        while (toRebind instanceof ChainedCursor) {
            toRebind = ((ChainedCursor) toRebind).getInput();
        }
        if (!(toRebind instanceof Rebindable))
            return null;
        return (Rebindable) toRebind;
    }

    private static QueryContext contextIfNull(QueryContext context, StoreAdapter adapter) {
        if (context == null) {
            return new SimpleQueryContext(adapter);
        }
        assert (context.getSession() != null);
        return new DelegatingContext(adapter, context);
    }

    public static ChangeLevel commonChangeLevel(Collection<ChangeSet> changeSets) {
        ChangeLevel level = null;
        for (ChangeSet cs : changeSets) {
            if (level == null) {
                level = ChangeLevel.valueOf(cs.getChangeLevel());
            } else if (!level.name().equals(cs.getChangeLevel())) {
                throw new IllegalStateException("Mixed ChangeLevels: " + changeSets);
            }
        }
        assert (level != null);
        return level;
    }

    /** Check if {@code table} is the post-transform/online DDL state. Use to avoid skip double-handling a row. */
    private static boolean isTransformedTable(TableTransform transform, Table table) {
        return (transform.rowType.table() == table);
    }

    private static Row transformRow(QueryContext context, QueryBindings bindings, TableTransform transform,
            Row origRow) {
        final Row newRow;
        if (transform.projectedRowType != null) {
            List<? extends TPreparedExpression> pProjections = transform.projectedRowType.getProjections();
            newRow = new ProjectedRow(transform.projectedRowType, origRow, context, bindings,
                    ProjectedRow.createTEvaluatableExpressions(pProjections));
        } else {
            newRow = new OverlayingRow(origRow, transform.rowType);
        }
        return newRow;
    }

    //
    // Classes
    //

    private interface RowHandler {
        void handleRow(Row row);
    }

    /**
     * Helper for saving concurrently handled rows.
     * Concrete implementations *must* be thread safe.
     */
    private interface HKeySaver {
        void save(SchemaManager sm, Session session, Key hKey);
    }

    /**
     * Helper for checking for concurrently handled rows.
     * Must *only* be called with increasing hKeys and thrown away when the transaction closes.
     */
    private interface HKeyChecker {
        boolean contains(SchemaManager sm, Session session, Key hKey);
    }

    private static class SchemaManagerSaver implements HKeySaver {
        private final int tableID;

        private SchemaManagerSaver(int tableID) {
            this.tableID = tableID;
        }

        @Override
        public void save(SchemaManager sm, Session session, Key hKey) {
            sm.addOnlineHandledHKey(session, tableID, hKey);
        }
    }

    private static class SchemaManagerChecker implements HKeyChecker {
        private final int tableID;
        private Iterator<byte[]> iter;
        private KeyState last;

        private SchemaManagerChecker(int tableID) {
            this.tableID = tableID;
        }

        private void advance() {
            byte[] bytes = iter.next();
            last = (bytes != null) ? new KeyState(bytes) : null;
        }

        @Override
        public boolean contains(SchemaManager sm, Session session, Key hKey) {
            if (iter == null) {
                iter = sm.getOnlineHandledHKeyIterator(session, tableID, hKey);
                advance();
            }
            // Can scan until we reach, or go past, hKey. If past, can't skip.
            while (last != null) {
                int ret = last.compareTo(hKey);
                if (ret == 0) {
                    return true; // Match
                }
                if (ret > 0) {
                    return false; // last from iterator is ahead of hKey
                }
                advance();
            }
            // Iterator exhausted: no more to skip
            return false;
        }
    }

    private static class FalseChecker implements HKeyChecker {
        @Override
        public boolean contains(SchemaManager sm, Session session, Key hKey) {
            return false;
        }
    }

    /** Holds information about how to maintain/populate the new/modified instance of a table. */
    private static class TableTransform {
        public final ChangeLevel changeLevel;
        /** Target for concurrently handled DML. */
        public final HKeySaver hKeySaver;
        /** New row type for the table. */
        public final TableRowType rowType;
        /** Not {@code null} *iff* new rows need projected. */
        public final ProjectedTableRowType projectedRowType;
        /** Not {@code null} *iff* new rows need only be verified. */
        public final boolean checkConstraints;
        /** Contains table indexes to build (can be empty) */
        public final Collection<TableIndex> tableIndexes;
        /** Populated with group indexes to build (can be empty) */
        public final Collection<GroupIndex> groupIndexes;
        /** Used for CreateTableAs */
        public Operator deleteOperator;
        public Operator insertOperator;

        public TableTransform(ChangeLevel changeLevel, HKeySaver hKeySaver, TableRowType rowType,
                ProjectedTableRowType projectedRowType, boolean checkConstraints,
                Collection<TableIndex> tableIndexes, Collection<GroupIndex> groupIndexes, Operator deleteOperator,
                Operator insertOperator) {
            this.changeLevel = changeLevel;
            this.hKeySaver = hKeySaver;
            this.rowType = rowType;
            this.projectedRowType = projectedRowType;
            this.checkConstraints = checkConstraints;
            this.tableIndexes = tableIndexes;
            this.groupIndexes = groupIndexes;
            this.deleteOperator = deleteOperator;
            this.insertOperator = insertOperator;
        }
    }

    /** Table ID -> TableTransform */
    private static class TransformCache extends HashMap<Integer, TableTransform> {
    }
}