You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by vo...@apache.org on 2017/09/06 15:18:40 UTC

[1/2] ignite git commit: IGNITE-5572: SQL: ALTER TABLE ADD COLUMN support. This closes #2344.

Repository: ignite
Updated Branches:
  refs/heads/master ffaf10837 -> adec3e7ec


http://git-wip-us.apache.org/repos/asf/ignite/blob/adec3e7e/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/DynamicColumnsAbstractConcurrentSelfTest.java
----------------------------------------------------------------------
diff --git a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/DynamicColumnsAbstractConcurrentSelfTest.java b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/DynamicColumnsAbstractConcurrentSelfTest.java
new file mode 100644
index 0000000..969c985
--- /dev/null
+++ b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/DynamicColumnsAbstractConcurrentSelfTest.java
@@ -0,0 +1,1056 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.processors.cache.index;
+
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import javax.cache.Cache;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.IgniteCheckedException;
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.binary.BinaryObject;
+import org.apache.ignite.cache.CacheAtomicityMode;
+import org.apache.ignite.cache.CacheMode;
+import org.apache.ignite.cache.query.SqlQuery;
+import org.apache.ignite.cluster.ClusterNode;
+import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.internal.IgniteClientReconnectAbstractTest;
+import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.internal.IgniteInternalFuture;
+import org.apache.ignite.internal.managers.discovery.CustomEventListener;
+import org.apache.ignite.internal.processors.affinity.AffinityTopologyVersion;
+import org.apache.ignite.internal.processors.query.GridQueryProcessor;
+import org.apache.ignite.internal.processors.query.QueryField;
+import org.apache.ignite.internal.processors.query.QueryUtils;
+import org.apache.ignite.internal.processors.query.h2.IgniteH2Indexing;
+import org.apache.ignite.internal.processors.query.schema.SchemaOperationException;
+import org.apache.ignite.internal.processors.query.schema.message.SchemaFinishDiscoveryMessage;
+import org.apache.ignite.internal.util.typedef.F;
+import org.apache.ignite.internal.util.typedef.T3;
+import org.apache.ignite.internal.util.typedef.internal.U;
+import org.apache.ignite.lang.IgnitePredicate;
+
+import static org.apache.ignite.internal.IgniteClientReconnectAbstractTest.TestTcpDiscoverySpi;
+
+/**
+ * Concurrency tests for dynamic index create/drop.
+ */
+@SuppressWarnings("unchecked")
+public abstract class DynamicColumnsAbstractConcurrentSelfTest extends DynamicColumnsAbstractTest {
+    /** Test duration. */
+    private static final long TEST_DUR = 10_000L;
+
+    /** Large cache size. */
+    private static final int LARGE_CACHE_SIZE = 100_000;
+
+    /** Table name. */
+    private static final String TBL_NAME = "PERSON";
+
+    /** Cache name. */
+    private static final String CACHE_NAME = QueryUtils.createTableCacheName(QueryUtils.DFLT_SCHEMA, TBL_NAME);
+
+    /** Attribute to filter node out of cache data nodes. */
+    private static final String ATTR_FILTERED = "FILTERED";
+
+    /** SQL statement to create test table accompanied by template specification. */
+    private static final String CREATE_SQL_WITH_TEMPLATE = CREATE_SQL + " WITH \"template=TPL\"";
+
+    /** Latches to block certain index operations. */
+    private static final ConcurrentHashMap<UUID, T3<CountDownLatch, AtomicBoolean, CountDownLatch>> BLOCKS =
+        new ConcurrentHashMap<>();
+
+    /** Cache mode. */
+    private final CacheMode cacheMode;
+
+    /** Atomicity mode. */
+    private final CacheAtomicityMode atomicityMode;
+
+    /**
+     * Constructor.
+     *
+     * @param cacheMode Cache mode.
+     * @param atomicityMode Atomicity mode.
+     */
+    DynamicColumnsAbstractConcurrentSelfTest(CacheMode cacheMode, CacheAtomicityMode atomicityMode) {
+        this.cacheMode = cacheMode;
+        this.atomicityMode = atomicityMode;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void beforeTest() throws Exception {
+        super.beforeTest();
+
+        GridQueryProcessor.idxCls = BlockingIndexing.class;
+    }
+
+    /** {@inheritDoc} */
+    @SuppressWarnings("ConstantConditions")
+    @Override protected void afterTest() throws Exception {
+        GridQueryProcessor.idxCls = null;
+
+        for (T3<CountDownLatch, AtomicBoolean, CountDownLatch> block : BLOCKS.values())
+            block.get1().countDown();
+
+        BLOCKS.clear();
+
+        stopAllGrids();
+
+        super.afterTest();
+    }
+
+    /** {@inheritDoc} */
+    @Override protected long getTestTimeout() {
+        return 5 * 60 * 1000L;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected IgniteConfiguration commonConfiguration(int idx) throws Exception {
+        return super.commonConfiguration(idx).setDiscoverySpi(new TestTcpDiscoverySpi());
+    }
+
+    /**
+     * Make sure that coordinator migrates correctly between nodes.
+     *
+     * @throws Exception If failed.
+     */
+    public void testCoordinatorChange() throws Exception {
+        CountDownLatch finishLatch = new CountDownLatch(2);
+
+        // Start servers.
+        Ignite srv1 = ignitionStart(serverConfiguration(1), null);
+        Ignite srv2 = ignitionStart(serverConfiguration(2), null);
+        ignitionStart(serverConfiguration(3, true), finishLatch);
+
+        UUID srv1Id = srv1.cluster().localNode().id();
+        UUID srv2Id = srv2.cluster().localNode().id();
+
+        // Start client which will execute operations.
+        IgniteEx cli = (IgniteEx)ignitionStart(clientConfiguration(4), finishLatch);
+
+        createSqlCache(cli);
+
+        run(cli, CREATE_SQL_WITH_TEMPLATE);
+
+        // Test migration between normal servers.
+        CountDownLatch idxLatch = blockIndexing(srv1Id);
+
+        IgniteInternalFuture<?> colFut1 = addCols(cli, QueryUtils.DFLT_SCHEMA, c("age", Integer.class.getName()));
+
+        U.await(idxLatch);
+
+        //srv1.close();
+        Ignition.stop(srv1.name(), true);
+
+        unblockIndexing(srv1Id);
+
+        colFut1.get();
+
+        checkNodesState(TBL_NAME, c("age", Integer.class.getName()));
+
+        // Test migration from normal server to non-affinity server.
+        idxLatch = blockIndexing(srv2Id);
+
+        IgniteInternalFuture<?> colFut2 = addCols(cli, QueryUtils.DFLT_SCHEMA, c("city", String.class.getName()));
+
+        idxLatch.countDown();
+
+        //srv2.close();
+        Ignition.stop(srv2.name(), true);
+
+        U.await(idxLatch);
+
+        colFut2.get();
+
+        checkNodesState(TBL_NAME, c("city", String.class.getName()));
+    }
+
+    /**
+     * Test operations join.
+     *
+     * @throws Exception If failed.
+     */
+    public void testOperationChaining() throws Exception {
+        // 7 nodes * 2 columns = 14 latch countdowns.
+        CountDownLatch finishLatch = new CountDownLatch(14);
+
+        Ignite srv1 = ignitionStart(serverConfiguration(1), finishLatch);
+
+        ignitionStart(serverConfiguration(2), finishLatch);
+        ignitionStart(serverConfiguration(3, true), finishLatch);
+        ignitionStart(clientConfiguration(4), finishLatch);
+
+        createSqlCache(srv1);
+
+        run(srv1, CREATE_SQL_WITH_TEMPLATE);
+
+        CountDownLatch idxLatch = blockIndexing(srv1);
+
+        QueryField c1 = c("age", Integer.class.getName());
+        QueryField c2 = c("city", String.class.getName());
+
+        IgniteInternalFuture<?> colFut1 = addCols(srv1, QueryUtils.DFLT_SCHEMA, c1);
+
+        IgniteInternalFuture<?> colFut2 = addCols(srv1, QueryUtils.DFLT_SCHEMA, c2);
+
+        U.await(idxLatch);
+
+        // Start even more nodes of different flavors
+        ignitionStart(serverConfiguration(5), finishLatch);
+        ignitionStart(serverConfiguration(6, true), finishLatch);
+        ignitionStart(clientConfiguration(7), finishLatch);
+
+        assert !colFut1.isDone();
+        assert !colFut2.isDone();
+
+        unblockIndexing(srv1);
+
+        colFut1.get();
+        colFut2.get();
+
+        U.await(finishLatch);
+
+        checkNodesState(TBL_NAME, c1, c2);
+    }
+
+    /**
+     * Test node join on pending operation.
+     *
+     * @throws Exception If failed.
+     */
+    public void testNodeJoinOnPendingOperation() throws Exception {
+        CountDownLatch finishLatch = new CountDownLatch(4);
+
+        Ignite srv1 = ignitionStart(serverConfiguration(1), finishLatch);
+
+        createSqlCache(srv1);
+
+        run(srv1, CREATE_SQL_WITH_TEMPLATE);
+
+        CountDownLatch idxLatch = blockIndexing(srv1);
+
+        QueryField c = c("age", Integer.class.getName());
+
+        IgniteInternalFuture<?> idxFut = addCols(srv1, QueryUtils.DFLT_SCHEMA, c);
+
+        U.await(idxLatch);
+
+        ignitionStart(serverConfiguration(2), finishLatch);
+        ignitionStart(serverConfiguration(3, true), finishLatch);
+        ignitionStart(clientConfiguration(4), finishLatch);
+
+        assertFalse(idxFut.isDone());
+
+        unblockIndexing(srv1);
+
+        idxFut.get();
+
+        U.await(finishLatch);
+
+        checkNodesState(TBL_NAME, c);
+    }
+
+    /**
+     * PUT/REMOVE data from cache and add column concurrently.
+     *
+     * @throws Exception If failed,
+     */
+    public void testConcurrentPutRemove() throws Exception {
+        CountDownLatch finishLatch = new CountDownLatch(4);
+
+        // Start several nodes.
+        Ignite srv1 = ignitionStart(serverConfiguration(1), finishLatch);
+        ignitionStart(serverConfiguration(2), finishLatch);
+        ignitionStart(serverConfiguration(3), finishLatch);
+        ignitionStart(serverConfiguration(4), finishLatch);
+
+        awaitPartitionMapExchange();
+
+        createSqlCache(srv1);
+
+        run(srv1, CREATE_SQL_WITH_TEMPLATE);
+
+        // Start data change operations from several threads.
+        final AtomicBoolean stopped = new AtomicBoolean();
+
+        IgniteInternalFuture updateFut = multithreadedAsync(new Callable<Void>() {
+            @Override public Void call() throws Exception {
+                while (!stopped.get()) {
+                    Ignite node = grid(ThreadLocalRandom.current().nextInt(1, 5));
+
+                    int key = ThreadLocalRandom.current().nextInt(0, LARGE_CACHE_SIZE);
+                    int val = ThreadLocalRandom.current().nextInt();
+
+                    IgniteCache<BinaryObject, BinaryObject> cache = node.cache(CACHE_NAME);
+
+                    if (ThreadLocalRandom.current().nextBoolean())
+                        cache.put(key(node, key), val(node, val));
+                    else
+                        cache.remove(key(node, key));
+                }
+
+                return null;
+            }
+        }, 4);
+
+        // Let some to arrive.
+        Thread.sleep(500L);
+
+        addCols(srv1, QueryUtils.DFLT_SCHEMA, c("v", Integer.class.getName())).get();
+
+        // Stop updates once index is ready.
+        stopped.set(true);
+
+        updateFut.get();
+
+        finishLatch.await();
+
+        // Make sure new column is there.
+        checkNodesState(TBL_NAME, c("v", Integer.class.getName()));
+
+        run(srv1, "update person set \"v\" = case when mod(id, 2) <> 0 then substring(name, 7, length(name) - 6) " +
+            "else null end");
+
+        // Get expected values.
+        Set<Integer> expKeys = new HashSet<>();
+
+        IgniteCache<BinaryObject, BinaryObject> cache = srv1.cache(CACHE_NAME).withKeepBinary();
+
+        for (int i = 0; i < LARGE_CACHE_SIZE; i++) {
+            BinaryObject key = key(srv1, i);
+
+            BinaryObject val = cache.get(key);
+
+            if (val != null) {
+                int id = key.field("ID");
+
+                assertEquals(i, id);
+
+                if (id % 2 != 0)
+                    expKeys.add(i);
+            }
+        }
+
+        String valTypeName = ((IgniteEx)srv1).context().query().types(CACHE_NAME).iterator().next().valueTypeName();
+
+        // Validate query result.
+        for (Ignite node : Ignition.allGrids()) {
+            IgniteCache<BinaryObject, BinaryObject> nodeCache = node.cache(CACHE_NAME).withKeepBinary();
+
+            SqlQuery qry = new SqlQuery(valTypeName, "from " + TBL_NAME + " where mod(id, 2) <> 0");
+
+            List<Cache.Entry<BinaryObject, BinaryObject>> res = nodeCache.query(qry).getAll();
+
+            assertEquals("Cache size mismatch [exp=" + expKeys.size() + ", actual=" + res.size() + ']',
+                expKeys.size(), res.size());
+
+            for (Cache.Entry<BinaryObject, BinaryObject> entry : res) {
+                int key = entry.getKey().field("ID");
+                int v = entry.getValue().field("v");
+
+                String name = entry.getValue().field("NAME");
+
+                assertTrue("Expected key is not in result set: " + key, expKeys.contains(key));
+
+                assertEquals(Integer.parseInt(name.substring(6)), v);
+            }
+
+        }
+    }
+
+    /**
+     * @param node Node.
+     * @param val Number to form string field value from.
+     * @return PERSON cache value.
+     */
+    private BinaryObject val(Ignite node, int val) {
+        String valTypeName = ((IgniteEx)node).context().query().types(CACHE_NAME).iterator().next().valueTypeName();
+
+        return node.binary().builder(valTypeName).setField("name", "person" + val).build();
+    }
+
+    /**
+     * @param node Node.
+     * @param key Value for ID field.
+     * @return PERSON cache key.
+     */
+    private BinaryObject key(Ignite node, int key) {
+        String keyTypeName = ((IgniteEx)node).context().query().types(CACHE_NAME).iterator().next().keyTypeName();
+
+        return node.binary().builder(keyTypeName).setField("ID", key).build();
+    }
+
+    /**
+     * Test index consistency on re-balance.
+     *
+     * @throws Exception If failed.
+     */
+    public void testConcurrentRebalance() throws Exception {
+        // Start cache and populate it with data.
+        Ignite srv1 = ignitionStart(serverConfiguration(1));
+        Ignite srv2 = ignitionStart(serverConfiguration(2));
+
+        createSqlCache(srv1);
+
+        run(srv1, CREATE_SQL_WITH_TEMPLATE);
+
+        awaitPartitionMapExchange();
+
+        put(srv1, 0, LARGE_CACHE_SIZE);
+
+        // Start index operation in blocked state.
+        CountDownLatch idxLatch1 = blockIndexing(srv1);
+        CountDownLatch idxLatch2 = blockIndexing(srv2);
+
+        QueryField c = c("salary", Float.class.getName());
+
+        final IgniteInternalFuture<?> idxFut = addCols(srv1, QueryUtils.DFLT_SCHEMA, c);
+
+        U.await(idxLatch1);
+        U.await(idxLatch2);
+
+        // Start two more nodes and unblock index operation in the middle.
+        ignitionStart(serverConfiguration(3));
+
+        unblockIndexing(srv1);
+        unblockIndexing(srv2);
+
+        ignitionStart(serverConfiguration(4));
+
+        awaitPartitionMapExchange();
+
+        // Validate index state.
+        idxFut.get();
+
+        checkNodesState(TBL_NAME, c);
+    }
+
+    /**
+     * Put to cache keys and values for range from startIdx to endIdx.
+     * @param node Node.
+     * @param startIdx Starting index.
+     * @param endIdx Ending index.
+     */
+    private void put(Ignite node, int startIdx, int endIdx) {
+        for (int i = startIdx; i < endIdx; i++)
+            node.cache(CACHE_NAME).put(key(node, i), val(node, i));
+    }
+
+    /**
+     * Check what happens in case cache is destroyed before operation is started.
+     *
+     * @throws Exception If failed.
+     */
+    public void testConcurrentCacheDestroy() throws Exception {
+        // Start complex topology.
+        Ignite srv1 = ignitionStart(serverConfiguration(1));
+
+        ignitionStart(serverConfiguration(2));
+        ignitionStart(serverConfiguration(3, true));
+
+        Ignite cli = ignitionStart(clientConfiguration(4));
+
+        // Start cache and populate it with data.
+        createSqlCache(cli);
+
+        run(cli, CREATE_SQL_WITH_TEMPLATE);
+
+        put(cli, 0, LARGE_CACHE_SIZE);
+
+        // Start index operation and block it on coordinator.
+        CountDownLatch idxLatch = blockIndexing(srv1);
+
+        QueryField c = c("city", String.class.getName());
+
+        final IgniteInternalFuture<?> idxFut = addCols(srv1, QueryUtils.DFLT_SCHEMA, c);
+
+        idxLatch.await();
+
+        // Destroy cache (drop table).
+        run(cli, DROP_SQL);
+
+        // Unblock indexing and see what happens.
+        unblockIndexing(srv1);
+
+        try {
+            idxFut.get();
+
+            fail("Exception has not been thrown.");
+        }
+        catch (SchemaOperationException e) {
+            // No-op.
+        }
+    }
+
+    /**
+     * Make sure that contended operations on the same index from different nodes do not hang when we issue both
+     * CREATE/DROP and SELECT statements.
+     *
+     * @throws Exception If failed.
+     */
+    public void testQueryConsistencyMultithreaded() throws Exception {
+        // Start complex topology.
+        ignitionStart(serverConfiguration(1));
+        ignitionStart(serverConfiguration(2));
+        ignitionStart(serverConfiguration(3, true));
+
+        Ignite cli = ignitionStart(clientConfiguration(4));
+
+        createSqlCache(cli);
+
+        run(cli, CREATE_SQL_WITH_TEMPLATE);
+
+        put(cli, 0, 5000);
+
+        final AtomicBoolean stopped = new AtomicBoolean();
+
+        final AtomicInteger dynColCnt = new AtomicInteger();
+
+        IgniteInternalFuture fut = multithreadedAsync(new Callable<Void>() {
+            @Override public Void call() throws Exception {
+                while (!stopped.get()) {
+                    Ignite node = grid(ThreadLocalRandom.current().nextInt(1, 5));
+
+                    IgniteInternalFuture fut = addCols(node, QueryUtils.DFLT_SCHEMA, c("newCol" + dynColCnt.getAndIncrement(),
+                        Integer.class.getName()));
+
+                    try {
+                        fut.get();
+                    }
+                    catch (SchemaOperationException e) {
+                        // No-op.
+                    }
+                    catch (Exception e) {
+                        fail("Unexpected exception: " + e);
+                    }
+                }
+
+                return null;
+            }
+        }, 1);
+
+        IgniteInternalFuture qryFut = multithreadedAsync(new Callable<Void>() {
+            @Override public Void call() throws Exception {
+                while (!stopped.get()) {
+                    Ignite node = grid(ThreadLocalRandom.current().nextInt(1, 5));
+
+                    IgniteCache<BinaryObject, BinaryObject> cache = node.cache(CACHE_NAME).withKeepBinary();
+
+                    String valTypeName = ((IgniteEx)node).context().query().types(CACHE_NAME)
+                        .iterator().next().valueTypeName();
+
+                    List<Cache.Entry<BinaryObject, BinaryObject>> res = cache.query(
+                        new SqlQuery<BinaryObject, BinaryObject>(valTypeName, "from " + TBL_NAME)).getAll();
+
+                    assertEquals(5000, res.size());
+                }
+
+                return null;
+            }
+        }, 8);
+
+        Thread.sleep(TEST_DUR);
+
+        stopped.set(true);
+
+        // Make sure nothing hanged.
+        fut.get();
+        qryFut.get();
+    }
+
+    /**
+     * Make sure that client receives schema changes made while it was disconnected.
+     *
+     * @throws Exception If failed.
+     */
+    public void testClientReconnect() throws Exception {
+        checkClientReconnect(false, true);
+    }
+
+    /**
+     * Make sure that client receives schema changes made while it was disconnected, even with cache recreation.
+     *
+     * @throws Exception If failed.
+     */
+    public void testClientReconnectWithCacheRestart() throws Exception {
+        checkClientReconnect(true, true);
+    }
+
+    /**
+     * Make sure that client receives schema changes made while it was disconnected.
+     *
+     * @throws Exception If failed.
+     */
+    public void testClientReconnectWithNonDynamicCache() throws Exception {
+        checkClientReconnect(false, false);
+    }
+
+    /**
+     * Make sure that client receives schema changes made while it was disconnected, even with cache recreation.
+     *
+     * @throws Exception If failed.
+     */
+    public void testClientReconnectWithNonDynamicCacheRestart() throws Exception {
+        checkClientReconnect(true, false);
+    }
+
+    /**
+     * Make sure that client receives schema changes made while it was disconnected, optionally with cache restart
+     * in the interim.
+     *
+     * @param restartCache Whether cache needs to be recreated during client's absence.
+     * @param dynamicCache Whether recreate, if needed, should be done on dynamic or static cache.
+     * @throws Exception If failed.
+     */
+    private void checkClientReconnect(final boolean restartCache, boolean dynamicCache) throws Exception {
+        // Start complex topology.
+        final Ignite srv = ignitionStart(serverConfiguration(1));
+        ignitionStart(serverConfiguration(2));
+        ignitionStart(serverConfiguration(3, true));
+
+        final Ignite cli = ignitionStart(clientConfiguration(4));
+
+        if (dynamicCache) {
+            createSqlCache(cli);
+
+            run(cli, CREATE_SQL_WITH_TEMPLATE);
+        }
+
+        final String schemaName = dynamicCache ? QueryUtils.DFLT_SCHEMA : "idx";
+
+        final QueryField[] cols =
+            new QueryField[] { c("age", Integer.class.getName()), c("city", String.class.getName()) };
+
+        // Check index create.
+        reconnectClientNode(srv, cli, restartCache, dynamicCache, new RunnableX() {
+            @Override public void run() throws Exception {
+                addCols(srv, schemaName, cols).get();
+            }
+        });
+
+        checkNodeState((IgniteEx)cli, schemaName, TBL_NAME, cols);
+    }
+
+    /**
+     * Reconnect the client and run specified actions while it's out.
+     *
+     * @param srvNode Server node.
+     * @param cliNode Client node.
+     * @param restart Whether cache has to be recreated prior to executing required actions.
+     * @param dynamicCache Whether recreate, if needed, should be done on dynamic or static cache.
+     * @param clo Closure to run
+     * @throws Exception If failed.
+     */
+    private void reconnectClientNode(final Ignite srvNode, final Ignite cliNode, final boolean restart,
+        final boolean dynamicCache, final RunnableX clo) throws Exception {
+        IgniteClientReconnectAbstractTest.reconnectClientNode(log, cliNode, srvNode, new Runnable() {
+            @Override public void run() {
+                if (restart) {
+                    if (dynamicCache) {
+                        DynamicColumnsAbstractConcurrentSelfTest.this.run(srvNode, DROP_SQL);
+
+                        DynamicColumnsAbstractConcurrentSelfTest.this.run(srvNode, CREATE_SQL_WITH_TEMPLATE);
+                    }
+                    else {
+                        srvNode.destroyCache("idx");
+
+                        CacheConfiguration ccfg;
+
+                        try {
+                            ccfg = clientConfiguration(0).getCacheConfiguration()[0];
+                        }
+                        catch (Exception e) {
+                            throw new AssertionError(e);
+                        }
+
+                        srvNode.createCache(ccfg);
+                    }
+                }
+
+                try {
+                    clo.run();
+                }
+                catch (Exception e) {
+                    throw new IgniteException("Test reconnect runnable failed.", e);
+                }
+            }
+        });
+
+        if (restart)
+            cliNode.cache(CACHE_NAME);
+    }
+
+    /**
+     * Test concurrent node start/stop along with index operations. Nothing should hang.
+     *
+     * @throws Exception If failed.
+     */
+    @SuppressWarnings("StringConcatenationInLoop")
+    public void testConcurrentOperationsAndNodeStartStopMultithreaded() throws Exception {
+        // Start several stable nodes.
+        ignitionStart(serverConfiguration(1));
+        ignitionStart(serverConfiguration(2));
+        ignitionStart(serverConfiguration(3, true));
+
+        final Ignite cli = ignitionStart(clientConfiguration(4));
+
+        createSqlCache(cli);
+
+        run(cli, CREATE_SQL_WITH_TEMPLATE);
+
+        final AtomicBoolean stopped = new AtomicBoolean();
+
+        // Start node start/stop worker.
+        final AtomicInteger nodeIdx = new AtomicInteger(4);
+
+        final AtomicInteger dynColCnt = new AtomicInteger();
+
+        IgniteInternalFuture startStopFut = multithreadedAsync(new Callable<Void>() {
+            @Override public Void call() throws Exception {
+                boolean exists = false;
+
+                int lastIdx = 0;
+
+                while (!stopped.get()) {
+                    if (exists) {
+                        stopGrid(lastIdx);
+
+                        exists = false;
+                    }
+                    else {
+                        lastIdx = nodeIdx.incrementAndGet();
+
+                        IgniteConfiguration cfg;
+
+                        switch (ThreadLocalRandom.current().nextInt(0, 3)) {
+                            case 1:
+                                cfg = serverConfiguration(lastIdx, false);
+
+                                break;
+
+                            case 2:
+
+                                cfg = serverConfiguration(lastIdx, true);
+
+                                break;
+
+                            default:
+                                cfg = clientConfiguration(lastIdx);
+                        }
+
+                        ignitionStart(cfg);
+
+                        exists = true;
+                    }
+
+                    Thread.sleep(ThreadLocalRandom.current().nextLong(500L, 1500L));
+                }
+
+                return null;
+            }
+        }, 1);
+
+        IgniteInternalFuture idxFut = multithreadedAsync(new Callable<Void>() {
+            @Override public Void call() throws Exception {
+                while (!stopped.get()) {
+                    Ignite node = grid(ThreadLocalRandom.current().nextInt(1, 5));
+
+                    IgniteInternalFuture fut = addCols(node, QueryUtils.DFLT_SCHEMA, c("newCol" + dynColCnt.getAndIncrement(),
+                        Integer.class.getName()));
+
+                    try {
+                        fut.get();
+                    }
+                    catch (SchemaOperationException e) {
+                        // No-op.
+                    }
+                    catch (Exception e) {
+                        fail("Unexpected exception: " + e);
+                    }
+                }
+
+                return null;
+            }
+        }, 1);
+
+        Thread.sleep(TEST_DUR);
+
+        stopped.set(true);
+
+        // Make sure nothing hanged.
+        startStopFut.get();
+        idxFut.get();
+
+        // Make sure cache is operational at this point.
+        createSqlCache(cli);
+
+        QueryField[] expCols = new QueryField[dynColCnt.get()];
+
+        // Too many index columns kills indexing internals, have to limit number of the columns
+        // to build the index on.
+        int idxColsCnt = Math.min(300, expCols.length);
+
+        Integer[] args = new Integer[idxColsCnt];
+
+        String updQry = "UPDATE " + TBL_NAME + " SET ";
+
+        String idxQry = "CREATE INDEX idx ON " + TBL_NAME + '(';
+
+        for (int i = 0; i < expCols.length; i++) {
+            expCols[i] = c("newCol" + i, Integer.class.getName());
+
+            if (i >= idxColsCnt)
+                continue;
+
+            if (i > 0) {
+                updQry += ", ";
+
+                idxQry += ", ";
+            }
+
+            updQry += "\"newCol" + i + "\" = id + ?";
+
+            idxQry += "\"newCol" + i + '"';
+
+            args[i] = i;
+        }
+
+        idxQry += ')';
+
+        checkNodesState(TBL_NAME, expCols);
+
+        put(cli, 0, 500);
+
+        run(cli.cache(CACHE_NAME), updQry, (Object[])args);
+
+        run(cli, idxQry);
+
+        run(cli, "DROP INDEX idx");
+    }
+
+    /**
+     * Block indexing.
+     *
+     * @param node Node.
+     */
+    @SuppressWarnings("SuspiciousMethodCalls")
+    private static CountDownLatch blockIndexing(Ignite node) {
+        UUID nodeId = ((IgniteEx)node).localNode().id();
+
+        return blockIndexing(nodeId);
+    }
+
+    /**
+     * Block indexing.
+     *
+     * @param nodeId Node.
+     */
+    @SuppressWarnings("SuspiciousMethodCalls")
+    private static CountDownLatch blockIndexing(UUID nodeId) {
+        assertFalse(BLOCKS.contains(nodeId));
+
+        CountDownLatch idxLatch = new CountDownLatch(1);
+
+        BLOCKS.put(nodeId, new T3<>(new CountDownLatch(1), new AtomicBoolean(), idxLatch));
+
+        return idxLatch;
+    }
+
+    /**
+     * Unblock indexing.
+     *
+     * @param node Node.
+     */
+    private static void unblockIndexing(Ignite node) {
+        UUID nodeId = ((IgniteEx)node).localNode().id();
+
+        unblockIndexing(nodeId);
+    }
+
+    /**
+     * Unblock indexing.
+     *
+     * @param nodeId Node ID.
+     */
+    @SuppressWarnings("ConstantConditions")
+    private static void unblockIndexing(UUID nodeId) {
+        T3<CountDownLatch, AtomicBoolean, CountDownLatch> blocker = BLOCKS.remove(nodeId);
+
+        assertNotNull(blocker);
+
+        blocker.get1().countDown();
+    }
+
+    /**
+     * Await indexing.
+     *
+     * @param nodeId Node ID.
+     */
+    @SuppressWarnings("ConstantConditions")
+    private static void awaitIndexing(UUID nodeId) {
+        T3<CountDownLatch, AtomicBoolean, CountDownLatch> blocker = BLOCKS.get(nodeId);
+
+        if (blocker != null) {
+            assertTrue(blocker.get2().compareAndSet(false, true));
+
+            blocker.get3().countDown();
+
+            while (true) {
+                try {
+                    blocker.get1().await();
+
+                    break;
+                }
+                catch (InterruptedException e) {
+                    // No-op.
+                }
+            }
+        }
+    }
+
+    /**
+     * Blocking indexing processor.
+     */
+    private static class BlockingIndexing extends IgniteH2Indexing {
+        /** {@inheritDoc} */
+        @Override public void dynamicAddColumn(String schemaName, String tblName, List<QueryField> cols,
+            boolean ifTblExists, boolean ifColNotExists)
+            throws IgniteCheckedException {
+            awaitIndexing(ctx.localNodeId());
+
+            super.dynamicAddColumn(schemaName, tblName, cols, ifTblExists, ifColNotExists);
+        }
+    }
+
+    /**
+     *
+     * @param node Target node.
+     * @param schemaName Schema name.
+     * @param flds Columns to add.
+     * @return DDL operation future.
+     */
+    private static IgniteInternalFuture<?> addCols(Ignite node, String schemaName, QueryField... flds) {
+        final String cacheName = F.eq(schemaName, QueryUtils.DFLT_SCHEMA) ? CACHE_NAME : "idx";
+
+        return ((IgniteEx)node).context().query().dynamicColumnAdd(cacheName, schemaName, TBL_NAME,
+            Arrays.asList(flds), false, false);
+    }
+
+    /**
+     * Start SQL cache on given node.
+     * @param node Node to create cache on.
+     * @return Created cache.
+     */
+    private IgniteCache<?, ?> createSqlCache(Ignite node) throws IgniteCheckedException {
+        node.addCacheConfiguration(new CacheConfiguration<>("TPL")
+            .setCacheMode(cacheMode)
+            .setAtomicityMode(atomicityMode)
+            .setNodeFilter(new NodeFilter()));
+
+        return node.getOrCreateCache(new CacheConfiguration<>("idx").setIndexedTypes(Integer.class, Integer.class));
+    }
+
+    /**
+     * Spoof blocking indexing class and start new node.
+     * @param cfg Node configuration.
+     * @return New node.
+     */
+    private static Ignite ignitionStart(IgniteConfiguration cfg) {
+        return ignitionStart(cfg, null);
+    }
+
+    /**
+     * Spoof blocking indexing class and start new node.
+     * @param cfg Node configuration.
+     * @param latch Latch to await for ultimate completion of DDL operations.
+     * @return New node.
+     */
+    private static Ignite ignitionStart(IgniteConfiguration cfg, final CountDownLatch latch) {
+        // Have to do this for each starting node - see GridQueryProcessor ctor, it nulls
+        // idxCls static field on each call.
+        GridQueryProcessor.idxCls = BlockingIndexing.class;
+
+        IgniteEx node = (IgniteEx)Ignition.start(cfg);
+
+        if (latch != null)
+            node.context().discovery().setCustomEventListener(SchemaFinishDiscoveryMessage.class,
+                new CustomEventListener<SchemaFinishDiscoveryMessage>() {
+                    @Override public void onCustomEvent(AffinityTopologyVersion topVer, ClusterNode snd,
+                        SchemaFinishDiscoveryMessage msg) {
+                        latch.countDown();
+                    }
+                });
+
+        return node;
+    }
+
+    /**
+     * Create server configuration.
+     * @param nodeIdx Node index.
+     * @param filtered Whether this node should not be treated as affinity node.
+     * @return Configuration.
+     * @throws Exception if failed.
+     */
+    private IgniteConfiguration serverConfiguration(int nodeIdx, boolean filtered) throws Exception {
+        IgniteConfiguration cfg = serverConfiguration(nodeIdx);
+
+        if (filtered)
+            cfg.setUserAttributes(Collections.singletonMap(ATTR_FILTERED, true));
+
+        return cfg;
+    }
+
+    /**
+     * Runnable which can throw checked exceptions.
+     */
+    interface RunnableX {
+        /**
+         * Do run.
+         *
+         * @throws Exception If failed.
+         */
+        @SuppressWarnings("UnnecessaryInterfaceModifier")
+        public void run() throws Exception;
+    }
+
+    /**
+     * Node filter.
+     */
+    protected static class NodeFilter implements IgnitePredicate<ClusterNode>, Serializable {
+        /** */
+        private static final long serialVersionUID = 0L;
+
+        /** {@inheritDoc} */
+        @Override public boolean apply(ClusterNode node) {
+            return node.attribute(ATTR_FILTERED) == null;
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/adec3e7e/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/DynamicColumnsAbstractTest.java
----------------------------------------------------------------------
diff --git a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/DynamicColumnsAbstractTest.java b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/DynamicColumnsAbstractTest.java
new file mode 100644
index 0000000..75b9a30
--- /dev/null
+++ b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/DynamicColumnsAbstractTest.java
@@ -0,0 +1,311 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.processors.cache.index;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.cache.QueryEntity;
+import org.apache.ignite.cache.query.SqlFieldsQuery;
+import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.configuration.MemoryConfiguration;
+import org.apache.ignite.configuration.MemoryPolicyConfiguration;
+import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.internal.processors.cache.DynamicCacheDescriptor;
+import org.apache.ignite.internal.processors.query.GridQueryTypeDescriptor;
+import org.apache.ignite.internal.processors.query.IgniteSQLException;
+import org.apache.ignite.internal.processors.query.QueryField;
+import org.apache.ignite.internal.processors.query.QuerySchema;
+import org.apache.ignite.internal.processors.query.QueryUtils;
+import org.apache.ignite.internal.processors.query.h2.IgniteH2Indexing;
+import org.apache.ignite.internal.processors.query.h2.opt.GridH2AbstractKeyValueRow;
+import org.apache.ignite.internal.processors.query.h2.opt.GridH2RowDescriptor;
+import org.apache.ignite.internal.processors.query.h2.opt.GridH2Table;
+import org.apache.ignite.internal.util.typedef.F;
+import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi;
+import org.apache.ignite.spi.discovery.tcp.ipfinder.TcpDiscoveryIpFinder;
+import org.apache.ignite.spi.discovery.tcp.ipfinder.vm.TcpDiscoveryVmIpFinder;
+import org.apache.ignite.testframework.GridTestUtils;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.h2.table.Column;
+import org.h2.value.DataType;
+
+/**
+ * Common stuff for dynamic columns tests.
+ */
+public abstract class DynamicColumnsAbstractTest extends GridCommonAbstractTest {
+    /** SQL to create test table. */
+    final static String CREATE_SQL = "CREATE TABLE IF NOT EXISTS Person (id int primary key, name varchar)";
+
+    /** SQL to drop test table. */
+    final static String DROP_SQL = "DROP TABLE Person";
+
+    /** IP finder. */
+    private static final TcpDiscoveryIpFinder IP_FINDER = new TcpDiscoveryVmIpFinder(true);
+
+    /**
+     * Check that given columns have been added to all related structures on target node exactly where needed
+     *    (namely, schema in cache descriptor, type descriptor on started cache, and H2 state on started cache).
+     * @param node Target node.
+     * @param schemaName Schema name to look for the table in.
+     * @param tblName Table name to check.
+     * @param cols Columns whose presence must be checked.
+     */
+    static void checkNodeState(IgniteEx node, String schemaName, String tblName, QueryField... cols) {
+        String cacheName = F.eq(schemaName, QueryUtils.DFLT_SCHEMA) ?
+            QueryUtils.createTableCacheName(schemaName, tblName) : schemaName;
+
+        // Schema state check - should pass regardless of cache state.
+        {
+            DynamicCacheDescriptor desc = node.context().cache().cacheDescriptor(cacheName);
+
+            assertNotNull("Cache descriptor not found", desc);
+
+            assertTrue(desc.sql() == F.eq(schemaName, QueryUtils.DFLT_SCHEMA));
+
+            QuerySchema schema = desc.schema();
+
+            assertNotNull(schema);
+
+            QueryEntity entity = null;
+
+            for (QueryEntity e : schema.entities()) {
+                if (F.eq(tblName, e.getTableName())) {
+                    entity = e;
+
+                    break;
+                }
+            }
+
+            assertNotNull("Query entity not found", entity);
+
+            Iterator<Map.Entry<String, String>> it = entity.getFields().entrySet().iterator();
+
+            for (int i = entity.getFields().size() - cols.length; i > 0 && it.hasNext(); i--)
+                it.next();
+
+            for (QueryField col : cols) {
+                assertTrue("New column not found in query entity: " + col.name(), it.hasNext());
+
+                Map.Entry<String, String> e = it.next();
+
+                assertEquals(col.name(), e.getKey());
+
+                assertEquals(col.typeName(), e.getValue());
+            }
+        }
+
+        // Start cache on this node if we haven't yet.
+        node.cache(cacheName);
+
+        // Type descriptor state check.
+        {
+            Collection<GridQueryTypeDescriptor> descs = node.context().query().types(cacheName);
+
+            GridQueryTypeDescriptor desc = null;
+
+            for (GridQueryTypeDescriptor d : descs) {
+                if (F.eq(tblName, d.tableName())) {
+                    desc = d;
+
+                    break;
+                }
+            }
+
+            assertNotNull("Type descriptor not found", desc);
+
+            Iterator<Map.Entry<String, Class<?>>> it = desc.fields().entrySet().iterator();
+
+            for (int i = desc.fields().size() - cols.length; i > 0 && it.hasNext(); i--)
+                it.next();
+
+            for (QueryField col : cols) {
+                assertTrue("New column not found in type descriptor: " + col.name(), it.hasNext());
+
+                Map.Entry<String, Class<?>> e = it.next();
+
+                assertEquals(col.name(), e.getKey());
+
+                assertEquals(col.typeName(), e.getValue().getName());
+            }
+        }
+
+        // H2 table state check.
+        {
+            GridH2Table tbl = ((IgniteH2Indexing)node.context().query().getIndexing()).dataTable(schemaName,
+                tblName);
+
+            assertNotNull("Table not found", tbl);
+
+            Iterator<Column> colIt = Arrays.asList(tbl.getColumns()).iterator();
+
+            GridH2RowDescriptor rowDesc = tbl.rowDescriptor();
+
+            int i = 0;
+
+            for (; i < tbl.getColumns().length - cols.length && colIt.hasNext(); i++)
+                colIt.next();
+
+            for (QueryField col : cols) {
+                assertTrue("New column not found in H2 table: " + col.name(), colIt.hasNext());
+
+                assertTrue(colIt.hasNext());
+
+                Column c = colIt.next();
+
+                assertEquals(col.name(), c.getName());
+
+                assertEquals(col.typeName(), DataType.getTypeClassName(c.getType()));
+
+                assertFalse(rowDesc.isKeyValueOrVersionColumn(i));
+
+                try {
+                    assertEquals(DataType.getTypeFromClass(Class.forName(col.typeName())),
+                        rowDesc.fieldType(i - GridH2AbstractKeyValueRow.DEFAULT_COLUMNS_COUNT));
+                }
+                catch (ClassNotFoundException e) {
+                    throw new AssertionError(e);
+                }
+
+                i++;
+            }
+        }
+    }
+
+    /**
+     * Check that given columns have been added to all related structures on all started nodes (namely, schema
+     *     in cache descriptor, type descriptor on started cache, and H2 state on started cache).
+     * @param tblName Table name to check.
+     * @param cols Columns whose presence must be checked.
+     */
+    static void checkNodesState(String tblName, QueryField... cols) {
+        for (Ignite node : Ignition.allGrids())
+            checkNodeState((IgniteEx)node, QueryUtils.DFLT_SCHEMA, tblName, cols);
+    }
+
+    /**
+     * @param name New column name.
+     * @param typeName Class name for this new column's data type.
+     * @return New column with given name and type.
+     */
+    protected static QueryField c(String name, String typeName) {
+        return new QueryField(name, typeName);
+    }
+
+    /**
+     * @param idx Node index.
+     * @return Client configuration.
+     * @throws Exception if failed.
+     */
+    protected IgniteConfiguration clientConfiguration(int idx) throws Exception {
+        QueryEntity e = new QueryEntity(Integer.class.getName(), "Person");
+
+        LinkedHashMap<String, String> flds = new LinkedHashMap<>();
+
+        flds.put("name", String.class.getName());
+
+        e.setFields(flds);
+
+        return commonConfiguration(idx).setClientMode(true).setCacheConfiguration(
+            new CacheConfiguration<>("idx").setQueryEntities(Collections.singletonList(e))
+        );
+    }
+
+    /**
+     * Create common node configuration.
+     *
+     * @param idx Index.
+     * @return Configuration.
+     * @throws Exception If failed.
+     */
+    protected IgniteConfiguration commonConfiguration(int idx) throws Exception {
+        IgniteConfiguration cfg = getConfiguration(getTestIgniteInstanceName(idx));
+
+        cfg.setDiscoverySpi(new TcpDiscoverySpi().setIpFinder(IP_FINDER));
+
+        MemoryConfiguration memCfg = new MemoryConfiguration()
+            .setDefaultMemoryPolicyName("default")
+            .setMemoryPolicies(
+                new MemoryPolicyConfiguration()
+                    .setName("default")
+                    .setMaxSize(32 * 1024 * 1024L)
+                    .setInitialSize(32 * 1024 * 1024L)
+            );
+
+        cfg.setMemoryConfiguration(memCfg);
+
+        return optimize(cfg);
+    }
+
+    /**
+     * Create server node configuration.
+     *
+     * @param idx Index.
+     * @return Configuration.
+     * @throws Exception If failed.
+     */
+    IgniteConfiguration serverConfiguration(int idx) throws Exception {
+        return commonConfiguration(idx);
+    }
+
+    /**
+     * Execute SQL command and return resulting dataset.
+     * @param node Node to run query from.
+     * @param sql Statement.
+     * @return result.
+     */
+    protected List<List<?>> run(Ignite node, String sql) {
+        return ((IgniteEx)node).context().query()
+            .querySqlFieldsNoCache(new SqlFieldsQuery(sql).setSchema(QueryUtils.DFLT_SCHEMA), true).getAll();
+    }
+
+    /**
+     * Execute SQL command and return resulting dataset.
+     * @param cache Cache to initiate query from.
+     * @param sql Statement.
+     * @return result.
+     */
+    protected List<List<?>> run(IgniteCache<?, ?> cache, String sql, Object... args) {
+        return cache.query(new SqlFieldsQuery(sql).setSchema(QueryUtils.DFLT_SCHEMA).setArgs(args)).getAll();
+    }
+
+    /**
+     * Run specified statement expected to throw {@code IgniteSqlException} with expected specified message.
+     * @param sql Statement.
+     * @param msg Expected message.
+     */
+    @SuppressWarnings("ThrowableResultOfMethodCallIgnored")
+    protected void assertThrows(final Ignite node, final String sql, String msg) {
+        GridTestUtils.assertThrows(log, new Callable<Object>() {
+            @Override public Object call() throws Exception {
+                run(node, sql);
+
+                return null;
+            }
+        }, IgniteSQLException.class, msg);
+    }
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/adec3e7e/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/DynamicColumnsConcurrentAtomicPartitionedSelfTest.java
----------------------------------------------------------------------
diff --git a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/DynamicColumnsConcurrentAtomicPartitionedSelfTest.java b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/DynamicColumnsConcurrentAtomicPartitionedSelfTest.java
new file mode 100644
index 0000000..ca68903
--- /dev/null
+++ b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/DynamicColumnsConcurrentAtomicPartitionedSelfTest.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.processors.cache.index;
+
+import org.apache.ignite.cache.CacheAtomicityMode;
+import org.apache.ignite.cache.CacheMode;
+
+/**
+ * Test to check concurrent operations on dynamic columns on ATOMIC PARTITIONED cache.
+ */
+public class DynamicColumnsConcurrentAtomicPartitionedSelfTest extends DynamicColumnsAbstractConcurrentSelfTest {
+    /**
+     * Constructor.
+     */
+    public DynamicColumnsConcurrentAtomicPartitionedSelfTest() {
+        super(CacheMode.PARTITIONED, CacheAtomicityMode.ATOMIC);
+    }
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/adec3e7e/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/DynamicColumnsConcurrentAtomicReplicatedSelfTest.java
----------------------------------------------------------------------
diff --git a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/DynamicColumnsConcurrentAtomicReplicatedSelfTest.java b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/DynamicColumnsConcurrentAtomicReplicatedSelfTest.java
new file mode 100644
index 0000000..9a3a32c
--- /dev/null
+++ b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/DynamicColumnsConcurrentAtomicReplicatedSelfTest.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.processors.cache.index;
+
+import org.apache.ignite.cache.CacheAtomicityMode;
+import org.apache.ignite.cache.CacheMode;
+
+/**
+ * Test to check concurrent operations on dynamic columns on ATOMIC REPLICATED cache.
+ */
+public class DynamicColumnsConcurrentAtomicReplicatedSelfTest extends DynamicColumnsAbstractConcurrentSelfTest {
+    /**
+     * Constructor.
+     */
+    public DynamicColumnsConcurrentAtomicReplicatedSelfTest() {
+        super(CacheMode.REPLICATED, CacheAtomicityMode.ATOMIC);
+    }
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/adec3e7e/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/DynamicColumnsConcurrentTransactionalPartitionedSelfTest.java
----------------------------------------------------------------------
diff --git a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/DynamicColumnsConcurrentTransactionalPartitionedSelfTest.java b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/DynamicColumnsConcurrentTransactionalPartitionedSelfTest.java
new file mode 100644
index 0000000..f42a447
--- /dev/null
+++ b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/DynamicColumnsConcurrentTransactionalPartitionedSelfTest.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.processors.cache.index;
+
+import org.apache.ignite.cache.CacheAtomicityMode;
+import org.apache.ignite.cache.CacheMode;
+
+/**
+ * Test to check concurrent operations on dynamic columns on TRANSACTIONAL PARTITIONED cache.
+ */
+public class DynamicColumnsConcurrentTransactionalPartitionedSelfTest extends DynamicColumnsAbstractConcurrentSelfTest {
+    /**
+     * Constructor.
+     */
+    public DynamicColumnsConcurrentTransactionalPartitionedSelfTest() {
+        super(CacheMode.PARTITIONED, CacheAtomicityMode.TRANSACTIONAL);
+    }
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/adec3e7e/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/DynamicColumnsConcurrentTransactionalReplicatedSelfTest.java
----------------------------------------------------------------------
diff --git a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/DynamicColumnsConcurrentTransactionalReplicatedSelfTest.java b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/DynamicColumnsConcurrentTransactionalReplicatedSelfTest.java
new file mode 100644
index 0000000..2b53e42
--- /dev/null
+++ b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/DynamicColumnsConcurrentTransactionalReplicatedSelfTest.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.processors.cache.index;
+
+import org.apache.ignite.cache.CacheAtomicityMode;
+import org.apache.ignite.cache.CacheMode;
+
+/**
+ * Test to check concurrent operations on dynamic columns on TRANSACTIONAL REPLICATED cache.
+ */
+public class DynamicColumnsConcurrentTransactionalReplicatedSelfTest extends DynamicColumnsAbstractConcurrentSelfTest {
+    /**
+     * Constructor.
+     */
+    public DynamicColumnsConcurrentTransactionalReplicatedSelfTest() {
+        super(CacheMode.REPLICATED, CacheAtomicityMode.TRANSACTIONAL);
+    }
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/adec3e7e/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/H2DynamicColumnsAbstractBasicSelfTest.java
----------------------------------------------------------------------
diff --git a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/H2DynamicColumnsAbstractBasicSelfTest.java b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/H2DynamicColumnsAbstractBasicSelfTest.java
new file mode 100644
index 0000000..651a3f7
--- /dev/null
+++ b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/H2DynamicColumnsAbstractBasicSelfTest.java
@@ -0,0 +1,348 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.processors.cache.index;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.binary.BinaryObject;
+import org.apache.ignite.cache.query.annotations.QuerySqlField;
+import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.internal.processors.query.QueryField;
+import org.apache.ignite.internal.processors.query.QueryUtils;
+
+/**
+ * Test to check dynamic columns related features.
+ */
+public abstract class H2DynamicColumnsAbstractBasicSelfTest extends DynamicColumnsAbstractTest {
+    /**
+     * Index of coordinator node.
+     */
+    final static int SRV_CRD_IDX = 0;
+
+    /**
+     * Index of non coordinator server node.
+     */
+    final static int SRV_IDX = 1;
+
+    /**
+     * Index of client.
+     */
+    final static int CLI_IDX = 2;
+
+    /** {@inheritDoc} */
+    @Override protected void beforeTestsStarted() throws Exception {
+        super.beforeTestsStarted();
+
+        for (IgniteConfiguration cfg : configurations())
+            Ignition.start(cfg);
+    }
+
+    /**
+     * @return Grid configurations to start.
+     * @throws Exception if failed.
+     */
+    private IgniteConfiguration[] configurations() throws Exception {
+        return new IgniteConfiguration[] {
+            commonConfiguration(0),
+            commonConfiguration(1),
+            clientConfiguration(2)
+        };
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void afterTestsStopped() throws Exception {
+        stopAllGrids();
+
+        super.afterTestsStopped();
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void beforeTest() throws Exception {
+        super.beforeTest();
+
+        run(CREATE_SQL);
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void afterTest() throws Exception {
+        run(DROP_SQL);
+
+        super.afterTest();
+    }
+
+    /**
+     * Test column addition to the end of the columns list.
+     */
+    public void testAddColumnSimple() {
+        run("ALTER TABLE Person ADD COLUMN age int");
+
+        doSleep(500);
+
+        QueryField c = c("AGE", Integer.class.getName());
+
+        for (Ignite node : Ignition.allGrids())
+            checkNodeState((IgniteEx)node, QueryUtils.DFLT_SCHEMA, "PERSON", c);
+    }
+
+    /**
+     * Test column addition to the end of the columns list.
+     */
+    public void testAddFewColumnsSimple() {
+        run("ALTER TABLE Person ADD COLUMN (age int, \"city\" varchar)");
+
+        doSleep(500);
+
+        for (Ignite node : Ignition.allGrids())
+            checkNodeState((IgniteEx)node, QueryUtils.DFLT_SCHEMA, "PERSON",
+                c("AGE", Integer.class.getName()),
+                c("city", String.class.getName()));
+    }
+
+    /**
+     * Test {@code IF EXISTS} handling.
+     */
+    public void testIfTableExists() {
+        run("ALTER TABLE if exists City ADD COLUMN population int");
+    }
+
+    /**
+     * Test {@code IF NOT EXISTS} handling.
+     */
+    public void testIfColumnNotExists() {
+        run("ALTER TABLE Person ADD COLUMN if not exists name varchar");
+    }
+
+    /**
+     * Test {@code IF NOT EXISTS} handling.
+     */
+    public void testDuplicateColumnName() {
+        assertThrows("ALTER TABLE Person ADD COLUMN name varchar", "Column already exists: NAME");
+    }
+
+    /**
+     * Test behavior in case of missing table.
+     */
+    public void testMissingTable() {
+        assertThrows("ALTER TABLE City ADD COLUMN name varchar", "Table doesn't exist: CITY");
+    }
+
+    /** */
+    @SuppressWarnings("unchecked")
+    public void testComplexOperations() {
+        IgniteCache<BinaryObject, BinaryObject> cache = ignite(nodeIndex())
+            .cache(QueryUtils.createTableCacheName(QueryUtils.DFLT_SCHEMA, "PERSON"));
+
+        run(cache, "ALTER TABLE Person ADD COLUMN city varchar");
+
+        run(cache, "INSERT INTO Person (id, name, city) values (1, 'John Doe', 'New York')");
+
+        run(cache, "INSERT INTO Person (id, name, city) values (2, 'Mike Watts', 'Denver')");
+
+        run(cache, "INSERT INTO Person (id, name, city) values (3, 'Ann Pierce', 'New York')");
+
+        run(cache, "CREATE INDEX pidx1 ON Person(name, city desc)");
+
+        CacheConfiguration<Integer, City> ccfg = defaultCacheConfiguration().setName("City")
+            .setIndexedTypes(Integer.class, City.class).setSqlSchema(QueryUtils.DFLT_SCHEMA);
+
+        ccfg.getQueryEntities().iterator().next().setKeyFieldName("id");
+
+        ignite(nodeIndex()).getOrCreateCache(ccfg);
+
+        run(cache, "ALTER TABLE City ADD COLUMN population int");
+
+        run(cache, "CREATE INDEX cidx1 ON City(population)");
+
+        run(cache, "CREATE INDEX cidx2 ON City(name)");
+
+        run(cache, "INSERT INTO City(id, name, population, state) values (5, 'New York', 15000000, 'New York')," +
+            "(7, 'Denver', 3000000, 'Colorado')");
+
+        List<List<?>> res = run(cache, "SELECT p.name from Person p join City c on p.city = c.name where " +
+            "c.population > 5000000 order by p.name");
+
+        assertEquals(2, res.size());
+
+        assertEquals(Collections.singletonList("Ann Pierce"), res.get(0));
+
+        assertEquals(Collections.singletonList("John Doe"), res.get(1));
+
+        run(cache, "ALTER TABLE Person ADD COLUMN age int");
+
+        run(cache, "UPDATE Person SET age = (5 - id) * 10");
+
+        res = run(cache, "SELECT p.name from Person p join City c on p.city = c.name where " +
+            "c.population > 5000000 and age < 40");
+
+        assertEquals(1, res.size());
+
+        assertEquals(Collections.singletonList("Ann Pierce"), res.get(0));
+
+        run(cache, "CREATE INDEX pidx2 on Person(age desc)");
+
+        run(cache, "DROP INDEX pidx2");
+
+        run(cache, "DROP INDEX pidx1");
+
+        run(cache, "DROP INDEX cidx2");
+
+        run(cache, "DROP INDEX cidx1");
+
+        run(cache, "DELETE FROM Person where age > 10");
+
+        assertEquals(0, cache.size());
+
+        ignite(nodeIndex()).destroyCache("City");
+    }
+
+    /**
+     * Test that we can add columns dynamically to tables associated with non dynamic caches as well.
+     */
+    public void testAddColumnToNonDynamicCache() {
+        run("ALTER TABLE \"idx\".PERSON ADD COLUMN CITY varchar");
+
+        doSleep(500);
+
+        QueryField c = c("CITY", String.class.getName());
+
+        for (Ignite node : Ignition.allGrids())
+            checkNodeState((IgniteEx)node, "idx", "PERSON", c);
+    }
+
+    /**
+     * Test that we can add columns dynamically to tables associated with non dynamic caches storing user types as well.
+     */
+    @SuppressWarnings("unchecked")
+    public void testAddColumnToNonDynamicCacheWithRealValueType() {
+        CacheConfiguration<Integer, City> ccfg = defaultCacheConfiguration().setName("City")
+            .setIndexedTypes(Integer.class, City.class);
+
+        IgniteCache<Integer, City> cache = ignite(nodeIndex()).getOrCreateCache(ccfg);
+
+        run(cache, "ALTER TABLE \"City\".City ADD COLUMN population int");
+
+        doSleep(500);
+
+        QueryField c = c("POPULATION", Integer.class.getName());
+
+        for (Ignite node : Ignition.allGrids())
+            checkNodeState((IgniteEx)node, "City", "CITY", c);
+
+        run(cache, "INSERT INTO \"City\".City (_key, id, name, state, population) values " +
+            "(1, 1, 'Washington', 'DC', 2500000)");
+
+        List<List<?>> res = run(cache, "select _key, id, name, state, population from \"City\".City");
+
+        assertEquals(Collections.singletonList(Arrays.asList(1, 1, "Washington", "DC", 2500000)), res);
+
+        City city = cache.get(1);
+
+        assertEquals(1, city.id());
+        assertEquals("Washington", city.name());
+        assertEquals("DC", city.state());
+
+        cache.destroy();
+    }
+
+    /**
+     * @return Node index to run queries on.
+     */
+    protected abstract int nodeIndex();
+
+    /**
+     * Run specified statement expected to throw {@code IgniteSqlException} with expected specified message.
+     * @param sql Statement.
+     * @param msg Expected message.
+     */
+    @SuppressWarnings("ThrowableResultOfMethodCallIgnored")
+    protected void assertThrows(final String sql, String msg) {
+        assertThrows(grid(nodeIndex()), sql, msg);
+    }
+
+    /**
+     * Execute SQL command and return resulting dataset.
+     * @param sql Statement.
+     * @return result.
+     */
+    protected List<List<?>> run(String sql) {
+        return run(grid(nodeIndex()), sql);
+    }
+
+    /** City class. */
+    private final static class City {
+        /** City id. */
+        @QuerySqlField
+        private int id;
+
+        /** City name. */
+        @QuerySqlField
+        private String name;
+
+        /** City state. */
+        @QuerySqlField
+        private String state;
+
+        /**
+         * @return City id.
+         */
+        public int id() {
+            return id;
+        }
+
+        /**
+         * @param id City id.
+         */
+        public void id(int id) {
+            this.id = id;
+        }
+
+        /**
+         * @return City name.
+         */
+        public String name() {
+            return name;
+        }
+
+        /**
+         * @param name City name.
+         */
+        public void name(String name) {
+            this.name = name;
+        }
+
+        /**
+         * @return City state.
+         */
+        public String state() {
+            return state;
+        }
+
+        /**
+         * @param state City state.
+         */
+        public void state(String state) {
+            this.state = state;
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/adec3e7e/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/H2DynamicColumnsClientBasicSelfTest.java
----------------------------------------------------------------------
diff --git a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/H2DynamicColumnsClientBasicSelfTest.java b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/H2DynamicColumnsClientBasicSelfTest.java
new file mode 100644
index 0000000..1972f2c
--- /dev/null
+++ b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/H2DynamicColumnsClientBasicSelfTest.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.processors.cache.index;
+
+/**
+ * Test to check {@code ALTER TABLE} operations started from client node.
+ */
+public class H2DynamicColumnsClientBasicSelfTest extends H2DynamicColumnsAbstractBasicSelfTest {
+    /** {@inheritDoc} */
+    @Override protected int nodeIndex() {
+        return CLI_IDX;
+    }
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/adec3e7e/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/H2DynamicColumnsServerBasicSelfTest.java
----------------------------------------------------------------------
diff --git a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/H2DynamicColumnsServerBasicSelfTest.java b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/H2DynamicColumnsServerBasicSelfTest.java
new file mode 100644
index 0000000..fa15752
--- /dev/null
+++ b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/H2DynamicColumnsServerBasicSelfTest.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.processors.cache.index;
+
+/**
+ * Test to check {@code ALTER TABLE} operations started from server node.
+ */
+public class H2DynamicColumnsServerBasicSelfTest extends H2DynamicColumnsAbstractBasicSelfTest {
+    /** {@inheritDoc} */
+    @Override protected int nodeIndex() {
+        return SRV_IDX;
+    }
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/adec3e7e/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/H2DynamicColumnsServerCoordinatorBasicSelfTest.java
----------------------------------------------------------------------
diff --git a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/H2DynamicColumnsServerCoordinatorBasicSelfTest.java b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/H2DynamicColumnsServerCoordinatorBasicSelfTest.java
new file mode 100644
index 0000000..db7f75c
--- /dev/null
+++ b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/H2DynamicColumnsServerCoordinatorBasicSelfTest.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.processors.cache.index;
+
+/**
+ * Test to check {@code ALTER TABLE} operations started from coordinator node.
+ */
+public class H2DynamicColumnsServerCoordinatorBasicSelfTest extends H2DynamicColumnsAbstractBasicSelfTest {
+    /** {@inheritDoc} */
+    @Override protected int nodeIndex() {
+        return SRV_CRD_IDX;
+    }
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/adec3e7e/modules/indexing/src/test/java/org/apache/ignite/internal/processors/query/h2/sql/GridQueryParsingTest.java
----------------------------------------------------------------------
diff --git a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/query/h2/sql/GridQueryParsingTest.java b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/query/h2/sql/GridQueryParsingTest.java
index 329e580..770a375 100644
--- a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/query/h2/sql/GridQueryParsingTest.java
+++ b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/query/h2/sql/GridQueryParsingTest.java
@@ -599,7 +599,7 @@ public class GridQueryParsingTest extends GridCommonAbstractTest {
             IgniteSQLException.class, "No cache value related columns found");
 
         assertParseThrows("create table Person (id int primary key, age int not null) WITH \"cacheTemplate=cache\"",
-            IgniteSQLException.class, "Non nullable columns are forbidden");
+            IgniteSQLException.class, "Non nullable columns are not supported [colName=AGE]");
 
         assertParseThrows("create table Person (id int primary key, age int unique) WITH \"template=cache\"",
             IgniteSQLException.class, "Too many constraints - only PRIMARY KEY is supported for CREATE TABLE");
@@ -620,6 +620,44 @@ public class GridQueryParsingTest extends GridCommonAbstractTest {
             IgniteSQLException.class, "Direct specification of _KEY and _VAL columns is forbidden");
     }
 
+    /** */
+    public void testParseAlterTableAddColumn() throws Exception {
+        assertAlterTableAddColumnEquals(buildAlterTableAddColumn("SCH2", "Person", false, false,
+            c("COMPANY", Value.STRING)), "ALTER TABLE SCH2.Person ADD company varchar");
+
+        assertAlterTableAddColumnEquals(buildAlterTableAddColumn("SCH2", "Person", true, true,
+            c("COMPANY", Value.STRING)), "ALTER TABLE IF EXISTS SCH2.Person ADD if not exists company varchar");
+
+        assertAlterTableAddColumnEquals(buildAlterTableAddColumn("SCH2", "Person", false, true,
+            c("COMPANY", Value.STRING), c("city", Value.STRING)),
+            "ALTER TABLE IF EXISTS SCH2.Person ADD (company varchar, \"city\" varchar)");
+
+        assertAlterTableAddColumnEquals(buildAlterTableAddColumn("SCH2", "City", false, true,
+            c("POPULATION", Value.INT)), "ALTER TABLE IF EXISTS SCH2.\"City\" ADD (population int)");
+
+        // There's no table with such name, but H2 parsing does not fail just yet.
+        assertAlterTableAddColumnEquals(buildAlterTableAddColumn("SCH2", "City", false, false,
+            c("POPULATION", Value.INT)), "ALTER TABLE SCH2.\"City\" ADD (population int)");
+
+        assertAlterTableAddColumnEquals(buildAlterTableAddColumn("SCH2", "Person", true, false,
+            c("NAME", Value.STRING)), "ALTER TABLE SCH2.Person ADD if not exists name varchar");
+
+        // There's a column with such name, but H2 parsing does not fail just yet.
+        assertAlterTableAddColumnEquals(buildAlterTableAddColumn("SCH2", "Person", false, false,
+            c("NAME", Value.STRING)), "ALTER TABLE SCH2.Person ADD name varchar");
+
+        // IF NOT EXISTS with multiple columns.
+        assertParseThrows("ALTER TABLE IF EXISTS SCH2.Person ADD if not exists (company varchar, city varchar)",
+            DbException.class, null);
+
+        // Both BEFORE and AFTER keywords.
+        assertParseThrows("ALTER TABLE IF EXISTS SCH2.Person ADD if not exists company varchar before addrid",
+            IgniteSQLException.class, "ALTER TABLE ADD COLUMN BEFORE/AFTER is not supported");
+
+        // No such schema.
+        assertParseThrows("ALTER TABLE SCH3.\"Person\" ADD (city varchar)", DbException.class, null);
+    }
+
     /**
      * @param sql Statement.
      * @param exCls Exception class.
@@ -747,6 +785,58 @@ public class GridQueryParsingTest extends GridCommonAbstractTest {
     }
 
     /**
+     * Parse SQL and compare it to expected instance of ALTER TABLE.
+     */
+    private void assertAlterTableAddColumnEquals(GridSqlAlterTableAddColumn exp, String sql) throws Exception {
+        Prepared prepared = parse(sql);
+
+        GridSqlStatement stmt = new GridSqlQueryParser(false).parse(prepared);
+
+        assertTrue(stmt instanceof GridSqlAlterTableAddColumn);
+
+        assertAlterTableAddColumnEquals(exp, (GridSqlAlterTableAddColumn)stmt);
+    }
+
+    /** */
+    private static GridSqlAlterTableAddColumn buildAlterTableAddColumn(String schema, String tbl,
+        boolean ifNotExists, boolean ifTblExists, GridSqlColumn... cols) {
+        GridSqlAlterTableAddColumn res = new GridSqlAlterTableAddColumn();
+
+        res.schemaName(schema);
+
+        res.tableName(tbl);
+
+        res.ifNotExists(ifNotExists);
+
+        res.ifTableExists(ifTblExists);
+
+        res.columns(cols);
+
+        return res;
+    }
+
+    /**
+     * Test two instances of {@link GridSqlAlterTableAddColumn} for equality.
+     */
+    private static void assertAlterTableAddColumnEquals(GridSqlAlterTableAddColumn exp,
+        GridSqlAlterTableAddColumn actual) {
+        assertEqualsIgnoreCase(exp.schemaName(), actual.schemaName());
+        assertEqualsIgnoreCase(exp.tableName(), actual.tableName());
+        assertEquals(exp.columns().length, actual.columns().length);
+
+        for (int i = 0; i < exp.columns().length; i++) {
+            GridSqlColumn expCol = exp.columns()[i];
+            GridSqlColumn col = actual.columns()[i];
+
+            assertEquals(expCol.columnName(), col.columnName());
+            assertEquals(expCol.column().getType(), col.column().getType());
+        }
+
+        assertEquals(exp.ifNotExists(), actual.ifNotExists());
+        assertEquals(exp.ifTableExists(), actual.ifTableExists());
+    }
+
+    /**
      * @param name Column name.
      * @param type Column data type.
      * @return {@link GridSqlColumn} with given name and type.

http://git-wip-us.apache.org/repos/asf/ignite/blob/adec3e7e/modules/indexing/src/test/java/org/apache/ignite/testsuites/IgniteCacheQuerySelfTestSuite.java
----------------------------------------------------------------------
diff --git a/modules/indexing/src/test/java/org/apache/ignite/testsuites/IgniteCacheQuerySelfTestSuite.java b/modules/indexing/src/test/java/org/apache/ignite/testsuites/IgniteCacheQuerySelfTestSuite.java
index 5ac0655f..947bb4d 100644
--- a/modules/indexing/src/test/java/org/apache/ignite/testsuites/IgniteCacheQuerySelfTestSuite.java
+++ b/modules/indexing/src/test/java/org/apache/ignite/testsuites/IgniteCacheQuerySelfTestSuite.java
@@ -89,11 +89,18 @@ import org.apache.ignite.internal.processors.cache.distributed.replicated.Ignite
 import org.apache.ignite.internal.processors.cache.distributed.replicated.IgniteCacheReplicatedQueryP2PDisabledSelfTest;
 import org.apache.ignite.internal.processors.cache.distributed.replicated.IgniteCacheReplicatedQuerySelfTest;
 import org.apache.ignite.internal.processors.cache.index.DuplicateKeyValueClassesSelfTest;
+import org.apache.ignite.internal.processors.cache.index.DynamicColumnsConcurrentAtomicPartitionedSelfTest;
+import org.apache.ignite.internal.processors.cache.index.DynamicColumnsConcurrentAtomicReplicatedSelfTest;
+import org.apache.ignite.internal.processors.cache.index.DynamicColumnsConcurrentTransactionalPartitionedSelfTest;
+import org.apache.ignite.internal.processors.cache.index.DynamicColumnsConcurrentTransactionalReplicatedSelfTest;
 import org.apache.ignite.internal.processors.cache.index.DynamicIndexClientBasicSelfTest;
 import org.apache.ignite.internal.processors.cache.index.DynamicIndexServerBasicSelfTest;
 import org.apache.ignite.internal.processors.cache.index.DynamicIndexServerCoordinatorBasicSelfTest;
 import org.apache.ignite.internal.processors.cache.index.DynamicIndexServerNodeFIlterBasicSelfTest;
 import org.apache.ignite.internal.processors.cache.index.DynamicIndexServerNodeFilterCoordinatorBasicSelfTest;
+import org.apache.ignite.internal.processors.cache.index.H2DynamicColumnsClientBasicSelfTest;
+import org.apache.ignite.internal.processors.cache.index.H2DynamicColumnsServerBasicSelfTest;
+import org.apache.ignite.internal.processors.cache.index.H2DynamicColumnsServerCoordinatorBasicSelfTest;
 import org.apache.ignite.internal.processors.cache.index.H2DynamicIndexAtomicPartitionedNearSelfTest;
 import org.apache.ignite.internal.processors.cache.index.H2DynamicIndexAtomicPartitionedSelfTest;
 import org.apache.ignite.internal.processors.cache.index.H2DynamicIndexAtomicReplicatedSelfTest;
@@ -250,6 +257,13 @@ public class IgniteCacheQuerySelfTestSuite extends TestSuite {
         suite.addTestSuite(H2DynamicIndexAtomicPartitionedSelfTest.class);
         suite.addTestSuite(H2DynamicIndexAtomicPartitionedNearSelfTest.class);
         suite.addTestSuite(H2DynamicTableSelfTest.class);
+        suite.addTestSuite(H2DynamicColumnsClientBasicSelfTest.class);
+        suite.addTestSuite(H2DynamicColumnsServerBasicSelfTest.class);
+        suite.addTestSuite(H2DynamicColumnsServerCoordinatorBasicSelfTest.class);
+        suite.addTestSuite(DynamicColumnsConcurrentAtomicPartitionedSelfTest.class);
+        suite.addTestSuite(DynamicColumnsConcurrentTransactionalPartitionedSelfTest.class);
+        suite.addTestSuite(DynamicColumnsConcurrentAtomicReplicatedSelfTest.class);
+        suite.addTestSuite(DynamicColumnsConcurrentTransactionalReplicatedSelfTest.class);
 
         // DML+DDL.
         suite.addTestSuite(H2DynamicIndexingComplexClientAtomicPartitionedTest.class);


[2/2] ignite git commit: IGNITE-5572: SQL: ALTER TABLE ADD COLUMN support. This closes #2344.

Posted by vo...@apache.org.
IGNITE-5572: SQL: ALTER TABLE ADD COLUMN support. This closes #2344.


Project: http://git-wip-us.apache.org/repos/asf/ignite/repo
Commit: http://git-wip-us.apache.org/repos/asf/ignite/commit/adec3e7e
Tree: http://git-wip-us.apache.org/repos/asf/ignite/tree/adec3e7e
Diff: http://git-wip-us.apache.org/repos/asf/ignite/diff/adec3e7e

Branch: refs/heads/master
Commit: adec3e7ec20fe04aa37a34416253602bc0387f4f
Parents: ffaf108
Author: Alexander Paschenko <al...@gmail.com>
Authored: Wed Sep 6 18:18:23 2017 +0300
Committer: devozerov <pp...@gmail.com>
Committed: Wed Sep 6 18:18:23 2017 +0300

----------------------------------------------------------------------
 .../processors/query/GridQueryIndexing.java     |   15 +
 .../processors/query/GridQueryProcessor.java    |  132 ++-
 .../internal/processors/query/QueryField.java   |   64 ++
 .../internal/processors/query/QuerySchema.java  |   37 +-
 .../query/QueryTypeDescriptorImpl.java          |   17 +-
 .../query/schema/SchemaOperationWorker.java     |    2 +-
 .../SchemaAbstractAlterTableOperation.java      |   39 +
 .../SchemaAlterTableAddColumnOperation.java     |   96 ++
 ...IgniteClientCacheInitializationFailTest.java |    8 +
 .../processors/query/h2/H2RowDescriptor.java    |  105 +-
 .../processors/query/h2/H2TableEngine.java      |   13 +-
 .../processors/query/h2/IgniteH2Indexing.java   |   22 +-
 .../query/h2/ddl/DdlStatementsProcessor.java    |   50 +-
 .../processors/query/h2/opt/GridH2Table.java    |   55 +-
 .../h2/sql/GridSqlAlterTableAddColumn.java      |  113 ++
 .../query/h2/sql/GridSqlQueryParser.java        |  160 ++-
 ...ynamicColumnsAbstractConcurrentSelfTest.java | 1056 ++++++++++++++++++
 .../cache/index/DynamicColumnsAbstractTest.java |  311 ++++++
 ...umnsConcurrentAtomicPartitionedSelfTest.java |   33 +
 ...lumnsConcurrentAtomicReplicatedSelfTest.java |   33 +
 ...currentTransactionalPartitionedSelfTest.java |   33 +
 ...ncurrentTransactionalReplicatedSelfTest.java |   33 +
 .../H2DynamicColumnsAbstractBasicSelfTest.java  |  348 ++++++
 .../H2DynamicColumnsClientBasicSelfTest.java    |   28 +
 .../H2DynamicColumnsServerBasicSelfTest.java    |   28 +
 ...icColumnsServerCoordinatorBasicSelfTest.java |   28 +
 .../query/h2/sql/GridQueryParsingTest.java      |   92 +-
 .../IgniteCacheQuerySelfTestSuite.java          |   14 +
 28 files changed, 2841 insertions(+), 124 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ignite/blob/adec3e7e/modules/core/src/main/java/org/apache/ignite/internal/processors/query/GridQueryIndexing.java
----------------------------------------------------------------------
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/GridQueryIndexing.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/GridQueryIndexing.java
index 8eecfc2..cecc5dd 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/GridQueryIndexing.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/GridQueryIndexing.java
@@ -165,6 +165,21 @@ public interface GridQueryIndexing {
     public void dynamicIndexDrop(String schemaName, String idxName, boolean ifExists) throws IgniteCheckedException;
 
     /**
+     * Add columns to dynamic table.
+     *
+     * @param schemaName Schema name.
+     * @param tblName Table name.
+     * @param cols Columns to add.
+     * @param ifTblExists Ignore operation if target table does not exist (instead of throwing an error).
+     * @param ifColNotExists Ignore operation if column already exists (instead of throwing an error) - is honored only
+     *     for single column case.
+     * @throws IgniteCheckedException If failed.
+     */
+    @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
+    public void dynamicAddColumn(String schemaName, String tblName, List<QueryField> cols, boolean ifTblExists,
+        boolean ifColNotExists) throws IgniteCheckedException;
+
+    /**
      * Registers cache.
      *
      * @param cacheName Cache name.

http://git-wip-us.apache.org/repos/asf/ignite/blob/adec3e7e/modules/core/src/main/java/org/apache/ignite/internal/processors/query/GridQueryProcessor.java
----------------------------------------------------------------------
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/GridQueryProcessor.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/GridQueryProcessor.java
index d5ac8fc..ea4c576 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/GridQueryProcessor.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/GridQueryProcessor.java
@@ -69,6 +69,7 @@ import org.apache.ignite.internal.processors.cache.query.CacheQueryFuture;
 import org.apache.ignite.internal.processors.cache.query.CacheQueryType;
 import org.apache.ignite.internal.processors.cache.query.GridCacheQueryType;
 import org.apache.ignite.internal.processors.cache.version.GridCacheVersion;
+import org.apache.ignite.internal.processors.query.property.QueryBinaryProperty;
 import org.apache.ignite.internal.processors.query.schema.SchemaIndexCacheVisitor;
 import org.apache.ignite.internal.processors.query.schema.SchemaIndexCacheVisitorImpl;
 import org.apache.ignite.internal.processors.query.schema.SchemaIndexOperationCancellationToken;
@@ -81,6 +82,7 @@ import org.apache.ignite.internal.processors.query.schema.message.SchemaFinishDi
 import org.apache.ignite.internal.processors.query.schema.message.SchemaOperationStatusMessage;
 import org.apache.ignite.internal.processors.query.schema.message.SchemaProposeDiscoveryMessage;
 import org.apache.ignite.internal.processors.query.schema.operation.SchemaAbstractOperation;
+import org.apache.ignite.internal.processors.query.schema.operation.SchemaAlterTableAddColumnOperation;
 import org.apache.ignite.internal.processors.query.schema.operation.SchemaIndexCreateOperation;
 import org.apache.ignite.internal.processors.query.schema.operation.SchemaIndexDropOperation;
 import org.apache.ignite.internal.processors.timeout.GridTimeoutProcessor;
@@ -754,6 +756,16 @@ public class GridQueryProcessor extends GridProcessorAdapter {
 
                                         QueryUtils.processDynamicIndexChange(opDrop.indexName(), null, typeDesc);
                                     }
+                                    else if (op0 instanceof SchemaAlterTableAddColumnOperation) {
+                                        SchemaAlterTableAddColumnOperation opAddCol =
+                                            (SchemaAlterTableAddColumnOperation)op0;
+
+                                        QueryTypeDescriptorImpl typeDesc = tblTypMap.get(opAddCol.tableName());
+
+                                        assert typeDesc != null;
+
+                                        processDynamicAddColumn(typeDesc, opAddCol.columns());
+                                    }
                                     else
                                         assert false;
                                 }
@@ -966,6 +978,32 @@ public class GridQueryProcessor extends GridProcessorAdapter {
             else
                 type = oldIdx.typeDescriptor();
         }
+        else if (op instanceof SchemaAlterTableAddColumnOperation) {
+            SchemaAlterTableAddColumnOperation op0 = (SchemaAlterTableAddColumnOperation)op;
+
+            type = type(cacheName, op0.tableName());
+
+            if (type == null) {
+                if (op0.ifTableExists())
+                    nop = true;
+                else
+                    err = new SchemaOperationException(SchemaOperationException.CODE_TABLE_NOT_FOUND,
+                        op0.tableName());
+            }
+            else {
+                for (QueryField col : op0.columns()) {
+                    if (type.hasField(col.name())) {
+                        if (op0.ifNotExists()) {
+                            assert op0.columns().size() == 1;
+
+                            nop = true;
+                        }
+                        else
+                            err = new SchemaOperationException(SchemaOperationException.CODE_COLUMN_EXISTS, col.name());
+                    }
+                }
+            }
+        }
         else
             err = new SchemaOperationException("Unsupported operation: " + op);
 
@@ -1070,6 +1108,32 @@ public class GridQueryProcessor extends GridProcessorAdapter {
                     err = new SchemaOperationException(SchemaOperationException.CODE_INDEX_NOT_FOUND, idxName);
             }
         }
+        else if (op instanceof SchemaAlterTableAddColumnOperation) {
+            SchemaAlterTableAddColumnOperation op0 = (SchemaAlterTableAddColumnOperation)op;
+
+            QueryEntity e = tblMap.get(op0.tableName());
+
+            if (e == null) {
+                if (op0.ifTableExists())
+                    nop = true;
+                else
+                    err = new SchemaOperationException(SchemaOperationException.CODE_TABLE_NOT_FOUND,
+                        op0.tableName());
+            }
+            else {
+                for (QueryField fld : op0.columns()) {
+                    if (e.getFields().containsKey(fld.name())) {
+                        if (op0.ifNotExists()) {
+                            assert op0.columns().size() == 1;
+
+                            nop = true;
+                        }
+                        else
+                            err = new SchemaOperationException(SchemaOperationException.CODE_COLUMN_EXISTS, fld.name());
+                    }
+                }
+            }
+        }
         else
             err = new SchemaOperationException("Unsupported operation: " + op);
 
@@ -1180,9 +1244,7 @@ public class GridQueryProcessor extends GridProcessorAdapter {
 
                     idxs.put(idxKey, idxDesc);
                 }
-                else {
-                    assert op instanceof SchemaIndexDropOperation;
-
+                else if (op instanceof SchemaIndexDropOperation) {
                     SchemaIndexDropOperation op0 = (SchemaIndexDropOperation) op;
 
                     QueryUtils.processDynamicIndexChange(op0.indexName(), null, type);
@@ -1191,6 +1253,12 @@ public class GridQueryProcessor extends GridProcessorAdapter {
 
                     idxs.remove(idxKey);
                 }
+                else {
+                    assert op instanceof SchemaAlterTableAddColumnOperation;
+
+                    // No-op - all processing is done at "local" stage
+                    // as we must update both table and type descriptor atomically.
+                }
             }
             catch (IgniteCheckedException e) {
                 U.warn(log, "Failed to finish index operation [opId=" + op.id() + " op=" + op + ']', e);
@@ -1229,7 +1297,7 @@ public class GridQueryProcessor extends GridProcessorAdapter {
     }
 
     /**
-     * Process index operation.
+     * Process schema operation.
      *
      * @param op Operation.
      * @param type Type descriptor.
@@ -1237,7 +1305,8 @@ public class GridQueryProcessor extends GridProcessorAdapter {
      * @param cancelTok Cancel token.
      * @throws SchemaOperationException If failed.
      */
-    public void processIndexOperationLocal(SchemaAbstractOperation op, QueryTypeDescriptorImpl type, IgniteUuid depId,
+    @SuppressWarnings("StatementWithEmptyBody")
+    public void processSchemaOperationLocal(SchemaAbstractOperation op, QueryTypeDescriptorImpl type, IgniteUuid depId,
         SchemaIndexOperationCancellationToken cancelTok) throws SchemaOperationException {
         if (log.isDebugEnabled())
             log.debug("Started local index operation [opId=" + op.id() + ']');
@@ -1251,7 +1320,7 @@ public class GridQueryProcessor extends GridProcessorAdapter {
 
         try {
             if (op instanceof SchemaIndexCreateOperation) {
-                SchemaIndexCreateOperation op0 = (SchemaIndexCreateOperation) op;
+                SchemaIndexCreateOperation op0 = (SchemaIndexCreateOperation)op;
 
                 QueryIndexDescriptorImpl idxDesc = QueryUtils.createIndexDescriptor(type, op0.index());
 
@@ -1261,10 +1330,18 @@ public class GridQueryProcessor extends GridProcessorAdapter {
                 idx.dynamicIndexCreate(op0.schemaName(), op0.tableName(), idxDesc, op0.ifNotExists(), visitor);
             }
             else if (op instanceof SchemaIndexDropOperation) {
-                SchemaIndexDropOperation op0 = (SchemaIndexDropOperation) op;
+                SchemaIndexDropOperation op0 = (SchemaIndexDropOperation)op;
 
                 idx.dynamicIndexDrop(op0.schemaName(), op0.indexName(), op0.ifExists());
             }
+            else if (op instanceof SchemaAlterTableAddColumnOperation) {
+                SchemaAlterTableAddColumnOperation op0 = (SchemaAlterTableAddColumnOperation)op;
+
+                processDynamicAddColumn(type, op0.columns());
+
+                idx.dynamicAddColumn(op0.schemaName(), op0.tableName(), op0.columns(), op0.ifTableExists(),
+                    op0.ifNotExists());
+            }
             else
                 throw new SchemaOperationException("Unsupported operation: " + op);
         }
@@ -2063,6 +2140,23 @@ private IgniteInternalFuture<Object> rebuildIndexesFromHash(@Nullable final Stri
     }
 
     /**
+     * Entry point for add column procedure.
+     * @param schemaName Schema name.
+     * @param tblName Target table name.
+     * @param cols Columns to add.
+     * @param ifTblExists Ignore operation if target table doesn't exist.
+     * @param ifNotExists Ignore operation if column exists.
+     */
+    public IgniteInternalFuture<?> dynamicColumnAdd(String cacheName, String schemaName, String tblName,
+        List<QueryField> cols, boolean ifTblExists, boolean ifNotExists) {
+
+        SchemaAlterTableAddColumnOperation op = new SchemaAlterTableAddColumnOperation(UUID.randomUUID(), cacheName,
+            schemaName, tblName, cols, ifTblExists, ifNotExists);
+
+        return startIndexOperationDistributed(op);
+    }
+
+    /**
      * Start distributed index change operation.
      *
      * @param op Operation.
@@ -2130,6 +2224,30 @@ private IgniteInternalFuture<Object> rebuildIndexesFromHash(@Nullable final Stri
     }
 
     /**
+     * Update type descriptor with new fields metadata.
+     *
+     * @param d Type descriptor to update.
+     * @param cols Columns to add.
+     * @throws IgniteCheckedException If failed to update type descriptor.
+     */
+    private void processDynamicAddColumn(QueryTypeDescriptorImpl d, List<QueryField> cols) throws IgniteCheckedException {
+        List<GridQueryProperty> props = new ArrayList<>(cols.size());
+
+        for (QueryField col : cols) {
+            try {
+                props.add(new QueryBinaryProperty(ctx, col.name(), null, Class.forName(col.typeName()),
+                    false, null));
+            }
+            catch (ClassNotFoundException e) {
+                throw new SchemaOperationException("Class not found for new property: " + col.typeName());
+            }
+        }
+
+        for (GridQueryProperty p : props)
+            d.addProperty(p, true);
+    }
+
+    /**
      *
      * @param cacheName Cache name.
      * @param sql Query.

http://git-wip-us.apache.org/repos/asf/ignite/blob/adec3e7e/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryField.java
----------------------------------------------------------------------
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryField.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryField.java
new file mode 100644
index 0000000..968c287
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryField.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.processors.query;
+
+import org.apache.ignite.internal.util.typedef.internal.S;
+
+import java.io.Serializable;
+
+/**
+ * Query field metadata.
+ */
+public class QueryField implements Serializable {
+    /** */
+    private static final long serialVersionUID = 0L;
+
+    /** Field name. */
+    private final String name;
+
+    /** Class name for this field's values. */
+    private final String typeName;
+
+    /**
+     * @param name Field name.
+     * @param typeName Class name for this field's values.
+     */
+    public QueryField(String name, String typeName) {
+        this.name = name;
+        this.typeName = typeName;
+    }
+
+    /**
+     * @return Field name.
+     */
+    public String name() {
+        return name;
+    }
+
+    /**
+     * @return Class name for this field's values.
+     */
+    public String typeName() {
+        return typeName;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return S.toString(QueryField.class, this);
+    }
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/adec3e7e/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QuerySchema.java
----------------------------------------------------------------------
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QuerySchema.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QuerySchema.java
index 34a4502..ccbba93 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QuerySchema.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QuerySchema.java
@@ -17,21 +17,21 @@
 
 package org.apache.ignite.internal.processors.query;
 
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
 import org.apache.ignite.cache.QueryEntity;
 import org.apache.ignite.cache.QueryIndex;
 import org.apache.ignite.internal.processors.query.schema.message.SchemaFinishDiscoveryMessage;
 import org.apache.ignite.internal.processors.query.schema.operation.SchemaAbstractOperation;
+import org.apache.ignite.internal.processors.query.schema.operation.SchemaAlterTableAddColumnOperation;
 import org.apache.ignite.internal.processors.query.schema.operation.SchemaIndexCreateOperation;
 import org.apache.ignite.internal.processors.query.schema.operation.SchemaIndexDropOperation;
 import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.internal.util.typedef.internal.S;
 
-import java.io.Serializable;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.LinkedList;
-import java.util.List;
-
 /**
  * Dynamic cache schema.
  */
@@ -118,9 +118,7 @@ public class QuerySchema implements Serializable {
                     }
                 }
             }
-            else {
-                assert op instanceof SchemaIndexDropOperation;
-
+            else if (op instanceof SchemaIndexDropOperation) {
                 SchemaIndexDropOperation op0 = (SchemaIndexDropOperation)op;
 
                 for (QueryEntity entity : entities) {
@@ -147,6 +145,27 @@ public class QuerySchema implements Serializable {
                     }
                 }
             }
+            else {
+                assert op instanceof SchemaAlterTableAddColumnOperation;
+
+                SchemaAlterTableAddColumnOperation op0 = (SchemaAlterTableAddColumnOperation)op;
+
+                QueryEntity target = null;
+
+                for (QueryEntity entity : entities()) {
+                    if (F.eq(entity.getTableName(), op0.tableName())) {
+                        target = entity;
+
+                        break;
+                    }
+                }
+
+                if (target == null)
+                    return;
+
+                for (QueryField field : op0.columns())
+                    target.getFields().put(field.name(), field.typeName());
+            }
         }
     }
 

http://git-wip-us.apache.org/repos/asf/ignite/blob/adec3e7e/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryTypeDescriptorImpl.java
----------------------------------------------------------------------
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryTypeDescriptorImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryTypeDescriptorImpl.java
index 79b90e5..0b77c4c 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryTypeDescriptorImpl.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryTypeDescriptorImpl.java
@@ -17,6 +17,12 @@
 
 package org.apache.ignite.internal.processors.query;
 
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
 import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.cache.QueryIndexType;
 import org.apache.ignite.internal.util.tostring.GridToStringExclude;
@@ -25,13 +31,6 @@ import org.apache.ignite.internal.util.typedef.internal.A;
 import org.apache.ignite.internal.util.typedef.internal.S;
 import org.jetbrains.annotations.Nullable;
 
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.LinkedHashMap;
-import java.util.Map;
-
 /**
  * Descriptor of type.
  */
@@ -50,7 +49,7 @@ public class QueryTypeDescriptorImpl implements GridQueryTypeDescriptor {
 
     /** Value field names and types with preserved order. */
     @GridToStringInclude
-    private final Map<String, Class<?>> fields = new LinkedHashMap<>();
+    private final LinkedHashMap<String, Class<?>> fields = new LinkedHashMap<>();
 
     /** */
     @GridToStringExclude
@@ -155,7 +154,7 @@ public class QueryTypeDescriptorImpl implements GridQueryTypeDescriptor {
     }
 
     /** {@inheritDoc} */
-    @Override public Map<String, Class<?>> fields() {
+    @Override public LinkedHashMap<String, Class<?>> fields() {
         return fields;
     }
 

http://git-wip-us.apache.org/repos/asf/ignite/blob/adec3e7e/modules/core/src/main/java/org/apache/ignite/internal/processors/query/schema/SchemaOperationWorker.java
----------------------------------------------------------------------
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/schema/SchemaOperationWorker.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/schema/SchemaOperationWorker.java
index 06feecb..0bf4d22 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/schema/SchemaOperationWorker.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/schema/SchemaOperationWorker.java
@@ -105,7 +105,7 @@ public class SchemaOperationWorker extends GridWorker {
     @Override protected void body() throws InterruptedException, IgniteInterruptedCheckedException {
         try {
             // Execute.
-            qryProc.processIndexOperationLocal(op, type, depId, cancelToken);
+            qryProc.processSchemaOperationLocal(op, type, depId, cancelToken);
 
             fut.onDone();
         }

http://git-wip-us.apache.org/repos/asf/ignite/blob/adec3e7e/modules/core/src/main/java/org/apache/ignite/internal/processors/query/schema/operation/SchemaAbstractAlterTableOperation.java
----------------------------------------------------------------------
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/schema/operation/SchemaAbstractAlterTableOperation.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/schema/operation/SchemaAbstractAlterTableOperation.java
new file mode 100644
index 0000000..a33bc08
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/schema/operation/SchemaAbstractAlterTableOperation.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.processors.query.schema.operation;
+
+import java.util.UUID;
+
+/**
+ * Parent class for ALTER TABLE command variants.
+ */
+public class SchemaAbstractAlterTableOperation extends SchemaAbstractOperation {
+    /** */
+    private static final long serialVersionUID = 0L;
+
+    /**
+     * Constructor.
+     *
+     * @param opId Operation ID.
+     * @param cacheName Cache name.
+     * @param schemaName Schema name.
+     */
+    public SchemaAbstractAlterTableOperation(UUID opId, String cacheName, String schemaName) {
+        super(opId, cacheName, schemaName);
+    }
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/adec3e7e/modules/core/src/main/java/org/apache/ignite/internal/processors/query/schema/operation/SchemaAlterTableAddColumnOperation.java
----------------------------------------------------------------------
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/schema/operation/SchemaAlterTableAddColumnOperation.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/schema/operation/SchemaAlterTableAddColumnOperation.java
new file mode 100644
index 0000000..9bd109f
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/schema/operation/SchemaAlterTableAddColumnOperation.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.processors.query.schema.operation;
+
+import java.util.List;
+import java.util.UUID;
+import org.apache.ignite.internal.processors.query.QueryField;
+import org.apache.ignite.internal.util.typedef.internal.S;
+
+/**
+ * Schema index drop operation.
+ */
+public class SchemaAlterTableAddColumnOperation extends SchemaAbstractAlterTableOperation {
+    /** */
+    private static final long serialVersionUID = 0L;
+
+    /** Target table name. */
+    private final String tblName;
+
+    /** Columns to add. */
+    private final List<QueryField> cols;
+
+    /** Ignore operation if target table doesn't exist. */
+    private final boolean ifTblExists;
+
+    /** Ignore operation if column exists. */
+    private final boolean ifNotExists;
+
+    /**
+     * Constructor.
+     *
+     * @param opId Operation id.
+     * @param schemaName Schema name.
+     * @param tblName Target table name.
+     * @param cols Columns to add.
+     * @param ifTblExists Ignore operation if target table doesn't exist.
+     * @param ifNotExists Ignore operation if column exists.
+     */
+    public SchemaAlterTableAddColumnOperation(UUID opId, String cacheName, String schemaName, String tblName,
+        List<QueryField> cols, boolean ifTblExists, boolean ifNotExists) {
+        super(opId, cacheName, schemaName);
+
+        this.tblName = tblName;
+        this.cols = cols;
+        this.ifTblExists = ifTblExists;
+        this.ifNotExists = ifNotExists;
+    }
+
+    /**
+     * @return Ignore operation if table doesn't exist.
+     */
+    public boolean ifTableExists() {
+        return ifTblExists;
+    }
+
+    /**
+     * @return Columns to add.
+     */
+    public List<QueryField> columns() {
+        return cols;
+    }
+
+    /**
+     * @return Quietly abort this command if column exists (honored only in single column case).
+     */
+    public boolean ifNotExists() {
+        return ifNotExists;
+    }
+
+    /**
+     * @return Target table name.
+     */
+    public String tableName() {
+        return tblName;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return S.toString(SchemaAlterTableAddColumnOperation.class, this, "parent", super.toString());
+    }
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/adec3e7e/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/IgniteClientCacheInitializationFailTest.java
----------------------------------------------------------------------
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/IgniteClientCacheInitializationFailTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/IgniteClientCacheInitializationFailTest.java
index fa6dd70..c745d8a 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/IgniteClientCacheInitializationFailTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/IgniteClientCacheInitializationFailTest.java
@@ -49,6 +49,7 @@ import org.apache.ignite.internal.processors.query.GridQueryIndexing;
 import org.apache.ignite.internal.processors.query.GridQueryProcessor;
 import org.apache.ignite.internal.processors.query.GridQueryTypeDescriptor;
 import org.apache.ignite.internal.processors.query.GridRunningQueryInfo;
+import org.apache.ignite.internal.processors.query.QueryField;
 import org.apache.ignite.internal.processors.query.QueryIndexDescriptorImpl;
 import org.apache.ignite.internal.processors.query.schema.SchemaIndexCacheVisitor;
 import org.apache.ignite.internal.util.GridSpinBusyLock;
@@ -284,6 +285,13 @@ public class IgniteClientCacheInitializationFailTest extends GridCommonAbstractT
         }
 
         /** {@inheritDoc} */
+        @Override public void dynamicAddColumn(String schemaName, String tblName, List<QueryField> cols,
+                                               boolean ifTblExists, boolean ifColNotExists)
+            throws IgniteCheckedException {
+            // No-op.
+        }
+
+        /** {@inheritDoc} */
         @Override public void registerCache(String cacheName, String schemaName,
             GridCacheContext<?, ?> cctx) throws IgniteCheckedException {
             if (FAILED_CACHES.contains(cctx.name()) && cctx.kernalContext().clientNode())

http://git-wip-us.apache.org/repos/asf/ignite/blob/adec3e7e/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/H2RowDescriptor.java
----------------------------------------------------------------------
diff --git a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/H2RowDescriptor.java b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/H2RowDescriptor.java
index 31f0e69..e59404e 100644
--- a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/H2RowDescriptor.java
+++ b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/H2RowDescriptor.java
@@ -17,6 +17,15 @@
 
 package org.apache.ignite.internal.processors.query.h2;
 
+import java.math.BigDecimal;
+import java.sql.Date;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
 import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.internal.processors.cache.CacheObject;
 import org.apache.ignite.internal.processors.cache.GridCacheContext;
@@ -60,16 +69,6 @@ import org.h2.value.ValueUuid;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
-import java.math.BigDecimal;
-import java.sql.Date;
-import java.sql.Time;
-import java.sql.Timestamp;
-import java.util.Arrays;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.UUID;
-
 import static org.apache.ignite.internal.processors.query.h2.opt.GridH2AbstractKeyValueRow.DEFAULT_COLUMNS_COUNT;
 import static org.apache.ignite.internal.processors.query.h2.opt.GridH2AbstractKeyValueRow.KEY_COL;
 import static org.apache.ignite.internal.processors.query.h2.opt.GridH2AbstractKeyValueRow.VAL_COL;
@@ -89,10 +88,10 @@ public class H2RowDescriptor implements GridH2RowDescriptor {
     private final GridQueryTypeDescriptor type;
 
     /** */
-    private final String[] fields;
+    private volatile String[] fields;
 
     /** */
-    private final int[] fieldTypes;
+    private volatile int[] fieldTypes;
 
     /** */
     private final int keyType;
@@ -107,13 +106,13 @@ public class H2RowDescriptor implements GridH2RowDescriptor {
     private final GridUnsafeGuard guard;
 
     /** */
-    private final GridQueryProperty[] props;
+    private volatile GridQueryProperty[] props;
 
     /** Id of user-defined key column */
-    private final int keyAliasColumnId;
+    private volatile int keyAliasColId;
 
     /** Id of user-defined value column */
-    private final int valueAliasColumnId;
+    private volatile int valAliasColId;
 
     /**
      * Constructor.
@@ -134,6 +133,17 @@ public class H2RowDescriptor implements GridH2RowDescriptor {
 
         guard = schema.offheap() == null ? null : new GridUnsafeGuard();
 
+        keyType = DataType.getTypeFromClass(type.keyClass());
+        valType = DataType.getTypeFromClass(type.valueClass());
+
+        refreshMetadataFromTypeDescriptor();
+    }
+
+    /**
+     * Update metadata of this row descriptor according to current state of type descriptor.
+     */
+    @SuppressWarnings("WeakerAccess")
+    public final void refreshMetadataFromTypeDescriptor() {
         Map<String, Class<?>> allFields = new LinkedHashMap<>();
 
         allFields.putAll(type.fields());
@@ -147,9 +157,6 @@ public class H2RowDescriptor implements GridH2RowDescriptor {
         for (int i = 0; i < fieldTypes.length; i++)
             fieldTypes[i] = DataType.getTypeFromClass(classes[i]);
 
-        keyType = DataType.getTypeFromClass(type.keyClass());
-        valType = DataType.getTypeFromClass(type.valueClass());
-
         props = new GridQueryProperty[fields.length];
 
         for (int i = 0; i < fields.length; i++) {
@@ -160,12 +167,12 @@ public class H2RowDescriptor implements GridH2RowDescriptor {
             props[i] = p;
         }
 
-        final List<String> fieldsList = Arrays.asList(fields);
+        List<String> fieldsList = Arrays.asList(fields);
 
-        keyAliasColumnId =
+        keyAliasColId =
             (type.keyFieldName() != null) ? DEFAULT_COLUMNS_COUNT + fieldsList.indexOf(type.keyFieldAlias()) : -1;
 
-        valueAliasColumnId =
+        valAliasColId =
             (type.valueFieldName() != null) ? DEFAULT_COLUMNS_COUNT + fieldsList.indexOf(type.valueFieldAlias()) : -1;
     }
 
@@ -376,29 +383,29 @@ public class H2RowDescriptor implements GridH2RowDescriptor {
     }
 
     /** {@inheritDoc} */
-    @Override public boolean isKeyColumn(int columnId) {
-        assert columnId >= 0;
-        return columnId == KEY_COL || columnId == keyAliasColumnId;
+    @Override public boolean isKeyColumn(int colId) {
+        assert colId >= 0;
+        return colId == KEY_COL || colId == keyAliasColId;
     }
 
     /** {@inheritDoc} */
-    @Override public boolean isValueColumn(int columnId) {
-        assert columnId >= 0;
-        return columnId == VAL_COL || columnId == valueAliasColumnId;
+    @Override public boolean isValueColumn(int colId) {
+        assert colId >= 0;
+        return colId == VAL_COL || colId == valAliasColId;
     }
 
     /** {@inheritDoc} */
     @SuppressWarnings("RedundantIfStatement")
-    @Override public boolean isKeyValueOrVersionColumn(int columnId) {
-        assert columnId >= 0;
+    @Override public boolean isKeyValueOrVersionColumn(int colId) {
+        assert colId >= 0;
 
-        if (columnId < DEFAULT_COLUMNS_COUNT)
+        if (colId < DEFAULT_COLUMNS_COUNT)
             return true;
 
-        if (columnId == keyAliasColumnId)
+        if (colId == keyAliasColId)
             return true;
 
-        if (columnId == valueAliasColumnId)
+        if (colId == valAliasColId)
             return true;
 
         return false;
@@ -409,26 +416,26 @@ public class H2RowDescriptor implements GridH2RowDescriptor {
         assert masks != null;
         assert masks.length > 0;
 
-        if (keyAliasColumnId < 0)
+        if (keyAliasColId < 0)
             return (masks[KEY_COL] & mask) != 0;
         else
-            return (masks[KEY_COL] & mask) != 0 || (masks[keyAliasColumnId] & mask) != 0;
+            return (masks[KEY_COL] & mask) != 0 || (masks[keyAliasColId] & mask) != 0;
     }
 
     /** {@inheritDoc} */
-    @Override public void initValueCache(Value valCache[], Value key, Value value, Value version) {
+    @Override public void initValueCache(Value valCache[], Value key, Value val, Value ver) {
         assert valCache != null;
         assert valCache.length > 0;
 
         valCache[KEY_COL] = key;
-        valCache[VAL_COL] = value;
-        valCache[VER_COL] = version;
+        valCache[VAL_COL] = val;
+        valCache[VER_COL] = ver;
 
-        if (keyAliasColumnId > 0)
-            valCache[keyAliasColumnId] = key;
+        if (keyAliasColId > 0)
+            valCache[keyAliasColId] = key;
 
-        if (valueAliasColumnId > 0)
-            valCache[valueAliasColumnId] = value;
+        if (valAliasColId > 0)
+            valCache[valAliasColId] = val;
     }
 
     /** {@inheritDoc} */
@@ -440,8 +447,8 @@ public class H2RowDescriptor implements GridH2RowDescriptor {
         for (int idx = 0; idx < data.length; idx++)
             data[idx] = row.getValue(idx);
 
-        copyAliasColumnData(data, KEY_COL, keyAliasColumnId);
-        copyAliasColumnData(data, VAL_COL, valueAliasColumnId);
+        copyAliasColumnData(data, KEY_COL, keyAliasColId);
+        copyAliasColumnData(data, VAL_COL, valAliasColId);
 
         return new SimpleRow(data);
     }
@@ -466,16 +473,16 @@ public class H2RowDescriptor implements GridH2RowDescriptor {
 
     /** {@inheritDoc} */
     @Override public int getAlternativeColumnId(int colId) {
-        if (keyAliasColumnId > 0) {
+        if (keyAliasColId > 0) {
             if (colId == KEY_COL)
-                return keyAliasColumnId;
-            else if (colId == keyAliasColumnId)
+                return keyAliasColId;
+            else if (colId == keyAliasColId)
                 return KEY_COL;
         }
-        if (valueAliasColumnId > 0) {
+        if (valAliasColId > 0) {
             if (colId == VAL_COL)
-                return valueAliasColumnId;
-            else if (colId == valueAliasColumnId)
+                return valAliasColId;
+            else if (colId == valAliasColId)
                 return VAL_COL;
         }
 

http://git-wip-us.apache.org/repos/asf/ignite/blob/adec3e7e/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/H2TableEngine.java
----------------------------------------------------------------------
diff --git a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/H2TableEngine.java b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/H2TableEngine.java
index 6bdcc30..d3e9560 100644
--- a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/H2TableEngine.java
+++ b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/H2TableEngine.java
@@ -17,24 +17,21 @@
 
 package org.apache.ignite.internal.processors.query.h2;
 
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.Statement;
 import org.apache.ignite.internal.processors.query.h2.database.H2RowFactory;
-import org.apache.ignite.internal.processors.query.h2.opt.GridH2RowDescriptor;
 import org.apache.ignite.internal.processors.query.h2.opt.GridH2Table;
 import org.h2.api.TableEngine;
 import org.h2.command.ddl.CreateTableData;
 import org.h2.table.TableBase;
-import org.jetbrains.annotations.Nullable;
-
-import java.sql.Connection;
-import java.sql.SQLException;
-import java.sql.Statement;
 
 /**
  * H2 Table engine.
  */
 public class H2TableEngine implements TableEngine {
     /** */
-    private static GridH2RowDescriptor rowDesc0;
+    private static H2RowDescriptor rowDesc0;
 
     /** */
     private static H2RowFactory rowFactory0;
@@ -56,7 +53,7 @@ public class H2TableEngine implements TableEngine {
      * @throws SQLException If failed.
      * @return Created table.
      */
-    public static synchronized GridH2Table createTable(Connection conn, String sql, GridH2RowDescriptor rowDesc,
+    public static synchronized GridH2Table createTable(Connection conn, String sql, H2RowDescriptor rowDesc,
         H2RowFactory rowFactory, H2TableDescriptor tblDesc)
         throws SQLException {
         rowDesc0 = rowDesc;

http://git-wip-us.apache.org/repos/asf/ignite/blob/adec3e7e/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/IgniteH2Indexing.java
----------------------------------------------------------------------
diff --git a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/IgniteH2Indexing.java b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/IgniteH2Indexing.java
index 0f97a4b..dff82d7 100644
--- a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/IgniteH2Indexing.java
+++ b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/IgniteH2Indexing.java
@@ -90,6 +90,7 @@ import org.apache.ignite.internal.processors.query.GridQueryIndexing;
 import org.apache.ignite.internal.processors.query.GridQueryTypeDescriptor;
 import org.apache.ignite.internal.processors.query.GridRunningQueryInfo;
 import org.apache.ignite.internal.processors.query.IgniteSQLException;
+import org.apache.ignite.internal.processors.query.QueryField;
 import org.apache.ignite.internal.processors.query.QueryIndexDescriptorImpl;
 import org.apache.ignite.internal.processors.query.QueryUtils;
 import org.apache.ignite.internal.processors.query.h2.database.H2RowFactory;
@@ -708,6 +709,25 @@ public class IgniteH2Indexing implements GridQueryIndexing {
         executeSql(schemaName, sql);
     }
 
+    /** {@inheritDoc} */
+    @Override public void dynamicAddColumn(String schemaName, String tblName, List<QueryField> cols,
+        boolean ifTblExists, boolean ifColNotExists) throws IgniteCheckedException {
+        // Locate table.
+        H2Schema schema = schemas.get(schemaName);
+
+        H2TableDescriptor desc = (schema != null ? schema.tableByName(tblName) : null);
+
+        if (desc == null) {
+            if (!ifTblExists)
+                throw new IgniteCheckedException("Table not found in internal H2 database [schemaName=" + schemaName +
+                    ", tblName=" + tblName + ']');
+            else
+                return;
+        }
+
+        desc.table().addColumns(cols, ifColNotExists);
+    }
+
     /**
      * Execute DDL command.
      *
@@ -1628,7 +1648,7 @@ public class IgniteH2Indexing implements GridQueryIndexing {
         if (log.isDebugEnabled())
             log.debug("Creating DB table with SQL: " + sql);
 
-        GridH2RowDescriptor rowDesc = new H2RowDescriptor(this, tbl, tbl.type(), schema);
+        H2RowDescriptor rowDesc = new H2RowDescriptor(this, tbl, tbl.type(), schema);
 
         H2RowFactory rowFactory = tbl.rowFactory(rowDesc);
 

http://git-wip-us.apache.org/repos/asf/ignite/blob/adec3e7e/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/ddl/DdlStatementsProcessor.java
----------------------------------------------------------------------
diff --git a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/ddl/DdlStatementsProcessor.java b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/ddl/DdlStatementsProcessor.java
index 65e402d..f322010 100644
--- a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/ddl/DdlStatementsProcessor.java
+++ b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/ddl/DdlStatementsProcessor.java
@@ -18,6 +18,7 @@
 package org.apache.ignite.internal.processors.query.h2.ddl;
 
 import java.sql.PreparedStatement;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -34,9 +35,11 @@ import org.apache.ignite.internal.processors.cache.query.IgniteQueryErrorCode;
 import org.apache.ignite.internal.processors.query.GridQueryProperty;
 import org.apache.ignite.internal.processors.query.GridQueryTypeDescriptor;
 import org.apache.ignite.internal.processors.query.IgniteSQLException;
+import org.apache.ignite.internal.processors.query.QueryField;
 import org.apache.ignite.internal.processors.query.QueryUtils;
 import org.apache.ignite.internal.processors.query.h2.IgniteH2Indexing;
 import org.apache.ignite.internal.processors.query.h2.opt.GridH2Table;
+import org.apache.ignite.internal.processors.query.h2.sql.GridSqlAlterTableAddColumn;
 import org.apache.ignite.internal.processors.query.h2.sql.GridSqlColumn;
 import org.apache.ignite.internal.processors.query.h2.sql.GridSqlCreateIndex;
 import org.apache.ignite.internal.processors.query.h2.sql.GridSqlCreateTable;
@@ -48,6 +51,7 @@ import org.apache.ignite.internal.processors.query.schema.SchemaOperationExcepti
 import org.apache.ignite.internal.util.future.GridFinishedFuture;
 import org.apache.ignite.internal.util.typedef.F;
 import org.h2.command.Prepared;
+import org.h2.command.ddl.AlterTableAlterColumn;
 import org.h2.command.ddl.CreateIndex;
 import org.h2.command.ddl.CreateTable;
 import org.h2.command.ddl.DropIndex;
@@ -203,6 +207,50 @@ public class DdlStatementsProcessor {
                 else
                     ctx.query().dynamicTableDrop(tbl.cacheName(), cmd.tableName(), cmd.ifExists());
             }
+            else if (stmt0 instanceof GridSqlAlterTableAddColumn) {
+                GridSqlAlterTableAddColumn cmd = (GridSqlAlterTableAddColumn)stmt0;
+
+                GridH2Table tbl = idx.dataTable(cmd.schemaName(), cmd.tableName());
+
+                if (tbl == null && cmd.ifTableExists()) {
+                    ctx.cache().createMissingQueryCaches();
+
+                    tbl = idx.dataTable(cmd.schemaName(), cmd.tableName());
+                }
+
+                if (tbl == null) {
+                    if (!cmd.ifTableExists())
+                        throw new SchemaOperationException(SchemaOperationException.CODE_TABLE_NOT_FOUND,
+                            cmd.tableName());
+                }
+                else {
+                    List<QueryField> cols = new ArrayList<>(cmd.columns().length);
+
+                    for (GridSqlColumn col : cmd.columns()) {
+                        if (tbl.doesColumnExist(col.columnName())) {
+                            if ((!cmd.ifNotExists() || cmd.columns().length != 1)) {
+                                throw new SchemaOperationException(SchemaOperationException.CODE_COLUMN_EXISTS,
+                                    col.columnName());
+                            }
+                            else {
+                                cols = null;
+
+                                break;
+                            }
+                        }
+
+                        cols.add(new QueryField(col.columnName(),
+                            DataType.getTypeClassName(col.column().getType())));
+                    }
+
+                    if (cols != null) {
+                        assert tbl.rowDescriptor() != null;
+
+                        fut = ctx.query().dynamicColumnAdd(tbl.cacheName(), cmd.schemaName(),
+                            tbl.rowDescriptor().type().tableName(), cols, cmd.ifTableExists(), cmd.ifNotExists());
+                    }
+                }
+            }
             else
                 throw new IgniteSQLException("Unsupported DDL operation: " + sql,
                     IgniteQueryErrorCode.UNSUPPORTED_OPERATION);
@@ -311,6 +359,6 @@ public class DdlStatementsProcessor {
      */
     public static boolean isDdlStatement(Prepared cmd) {
         return cmd instanceof CreateIndex || cmd instanceof DropIndex || cmd instanceof CreateTable ||
-            cmd instanceof DropTable;
+            cmd instanceof DropTable || cmd instanceof AlterTableAlterColumn;
     }
 }

http://git-wip-us.apache.org/repos/asf/ignite/blob/adec3e7e/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/opt/GridH2Table.java
----------------------------------------------------------------------
diff --git a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/opt/GridH2Table.java b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/opt/GridH2Table.java
index 107e3bb..694346c 100644
--- a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/opt/GridH2Table.java
+++ b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/opt/GridH2Table.java
@@ -32,6 +32,9 @@ import org.apache.ignite.internal.processors.cache.GridCacheContext;
 import org.apache.ignite.internal.processors.cache.KeyCacheObject;
 import org.apache.ignite.internal.processors.cache.query.QueryTable;
 import org.apache.ignite.internal.processors.cache.version.GridCacheVersion;
+import org.apache.ignite.internal.processors.query.IgniteSQLException;
+import org.apache.ignite.internal.processors.query.QueryField;
+import org.apache.ignite.internal.processors.query.h2.H2RowDescriptor;
 import org.apache.ignite.internal.processors.query.h2.database.H2RowFactory;
 import org.apache.ignite.internal.processors.query.h2.database.H2TreeIndex;
 import org.apache.ignite.internal.util.offheap.unsafe.GridUnsafeMemory;
@@ -49,9 +52,11 @@ import org.h2.result.Row;
 import org.h2.result.SearchRow;
 import org.h2.result.SortOrder;
 import org.h2.schema.SchemaObject;
+import org.h2.table.Column;
 import org.h2.table.IndexColumn;
 import org.h2.table.TableBase;
 import org.h2.table.TableType;
+import org.h2.value.DataType;
 import org.h2.value.Value;
 import org.jetbrains.annotations.Nullable;
 import org.jsr166.ConcurrentHashMap8;
@@ -68,7 +73,7 @@ public class GridH2Table extends TableBase {
     private final GridCacheContext cctx;
 
     /** */
-    private final GridH2RowDescriptor desc;
+    private final H2RowDescriptor desc;
 
     /** */
     private volatile ArrayList<Index> idxs;
@@ -121,7 +126,7 @@ public class GridH2Table extends TableBase {
      * @param idxsFactory Indexes factory.
      * @param cctx Cache context.
      */
-    public GridH2Table(CreateTableData createTblData, GridH2RowDescriptor desc, H2RowFactory rowFactory,
+    public GridH2Table(CreateTableData createTblData, H2RowDescriptor desc, H2RowFactory rowFactory,
         GridH2SystemIndexFactory idxsFactory, GridCacheContext cctx) {
         super(createTblData);
 
@@ -949,4 +954,50 @@ public class GridH2Table extends TableBase {
 
         return null;
     }
+
+    /**
+     * Add new columns to this table.
+     *
+     * @param cols Columns to add.
+     * @param ifNotExists Ignore this command if {@code cols} has size of 1 and column with given name already exists.
+     */
+    public void addColumns(List<QueryField> cols, boolean ifNotExists) {
+        assert !ifNotExists || cols.size() == 1;
+
+        lock(true);
+
+        try {
+            int pos = columns.length;
+
+            Column[] newCols = new Column[columns.length + cols.size()];
+
+            // First, let's copy existing columns to new array
+            System.arraycopy(columns, 0, newCols, 0, columns.length);
+
+            // And now, let's add new columns
+            for (QueryField col : cols) {
+                if (doesColumnExist(col.name())) {
+                    if (ifNotExists && cols.size() == 1)
+                        return;
+                    else
+                        throw new IgniteSQLException("Column already exists [tblName=" + getName() +
+                            ", colName=" + col.name() + ']');
+                }
+
+                try {
+                    newCols[pos++] = new Column(col.name(), DataType.getTypeFromClass(Class.forName(col.typeName())));
+                }
+                catch (ClassNotFoundException e) {
+                    throw new IgniteSQLException("H2 data type not found for class: " + col.typeName(), e);
+                }
+            }
+
+            setColumns(newCols);
+
+            desc.refreshMetadataFromTypeDescriptor();
+        }
+        finally {
+            unlock(true);
+        }
+    }
 }
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ignite/blob/adec3e7e/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/sql/GridSqlAlterTableAddColumn.java
----------------------------------------------------------------------
diff --git a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/sql/GridSqlAlterTableAddColumn.java b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/sql/GridSqlAlterTableAddColumn.java
new file mode 100644
index 0000000..40423c7
--- /dev/null
+++ b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/sql/GridSqlAlterTableAddColumn.java
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.processors.query.h2.sql;
+
+/**
+ * ALTER TABLE ADD COLUMN command data holder.
+ */
+public class GridSqlAlterTableAddColumn extends GridSqlStatement {
+    /** Schema name. */
+    private String schemaName;
+
+    /** Target table name. */
+    private String tblName;
+
+    /** Columns to add. */
+    private GridSqlColumn[] cols;
+
+    /** Quietly abort this command if column exists (honored only in single column case). */
+    private boolean ifNotExists;
+
+    /** Quietly abort this command if target table does not exist. */
+    private boolean ifTblExists;
+
+    /**
+     * @return Columns to add.
+     */
+    public GridSqlColumn[] columns() {
+        return cols;
+    }
+
+    /**
+     * @param cols Columns to add.
+     */
+    public void columns(GridSqlColumn[] cols) {
+        this.cols = cols;
+    }
+
+    /**
+     * @return Quietly abort this command if column exists (honored only in single column case).
+     */
+    public boolean ifNotExists() {
+        return ifNotExists;
+    }
+
+    /**
+     * @param ifNotExists Quietly abort this command if column exists (honored only in single column case).
+     */
+    public void ifNotExists(boolean ifNotExists) {
+        this.ifNotExists = ifNotExists;
+    }
+
+    /**
+     * @return Quietly abort this command if target table does not exist.
+     */
+    public boolean ifTableExists() {
+        return ifTblExists;
+    }
+
+    /**
+     * @param ifTblExists Quietly abort this command if target table does not exist.
+     */
+    public void ifTableExists(boolean ifTblExists) {
+        this.ifTblExists = ifTblExists;
+    }
+
+    /**
+     * @return Target table name.
+     */
+    public String tableName() {
+        return tblName;
+    }
+
+    /**
+     * @param tblName Target table name.
+     */
+    public void tableName(String tblName) {
+        this.tblName = tblName;
+    }
+
+    /**
+     * @return Schema name.
+     */
+    public String schemaName() {
+        return schemaName;
+    }
+
+    /**
+     * @param schemaName Schema name.
+     */
+    public void schemaName(String schemaName) {
+        this.schemaName = schemaName;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getSQL() {
+        throw new UnsupportedOperationException();
+    }
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/adec3e7e/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/sql/GridSqlQueryParser.java
----------------------------------------------------------------------
diff --git a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/sql/GridSqlQueryParser.java b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/sql/GridSqlQueryParser.java
index 0d6a0b2..97e9d5d 100644
--- a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/sql/GridSqlQueryParser.java
+++ b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/sql/GridSqlQueryParser.java
@@ -38,8 +38,10 @@ import org.apache.ignite.internal.processors.query.QueryUtils;
 import org.apache.ignite.internal.util.typedef.F;
 import org.h2.command.Command;
 import org.h2.command.CommandContainer;
+import org.h2.command.CommandInterface;
 import org.h2.command.Prepared;
 import org.h2.command.ddl.AlterTableAddConstraint;
+import org.h2.command.ddl.AlterTableAlterColumn;
 import org.h2.command.ddl.CreateIndex;
 import org.h2.command.ddl.CreateTable;
 import org.h2.command.ddl.CreateTableData;
@@ -393,6 +395,30 @@ public class GridSqlQueryParser {
     private static final Getter<Column, Expression> COLUMN_CHECK_CONSTRAINT = getter(Column.class, "checkConstraint");
 
     /** */
+    private static final Getter<AlterTableAlterColumn, String> ALTER_COLUMN_TBL_NAME =
+        getter(AlterTableAlterColumn.class, "tableName");
+
+    /** */
+    private static final Getter<AlterTableAlterColumn, ArrayList<Column>> ALTER_COLUMN_NEW_COLS =
+        getter(AlterTableAlterColumn.class, "columnsToAdd");
+
+    /** */
+    private static final Getter<AlterTableAlterColumn, Boolean> ALTER_COLUMN_IF_NOT_EXISTS =
+        getter(AlterTableAlterColumn.class, "ifNotExists");
+
+    /** */
+    private static final Getter<AlterTableAlterColumn, Boolean> ALTER_COLUMN_IF_TBL_EXISTS =
+        getter(AlterTableAlterColumn.class, "ifTableExists");
+
+    /** */
+    private static final Getter<AlterTableAlterColumn, String> ALTER_COLUMN_BEFORE_COL =
+        getter(AlterTableAlterColumn.class, "addBefore");
+
+    /** */
+    private static final Getter<AlterTableAlterColumn, String> ALTER_COLUMN_AFTER_COL =
+        getter(AlterTableAlterColumn.class, "addAfter");
+
+    /** */
     private static final String PARAM_NAME_VALUE_SEPARATOR = "=";
 
     /** */
@@ -885,41 +911,8 @@ public class GridSqlQueryParser {
 
         LinkedHashMap<String, GridSqlColumn> cols = new LinkedHashMap<>(data.columns.size());
 
-        for (Column col : data.columns) {
-            if (col.isAutoIncrement())
-                throw new IgniteSQLException("AUTO_INCREMENT columns are not supported [colName=" + col.getName() + ']',
-                    IgniteQueryErrorCode.UNSUPPORTED_OPERATION);
-
-            if (!col.isNullable())
-                throw new IgniteSQLException("Non nullable columns are forbidden [colName=" + col.getName() + ']',
-                    IgniteQueryErrorCode.PARSING);
-
-            if (COLUMN_IS_COMPUTED.get(col))
-                throw new IgniteSQLException("Computed columns are not supported [colName=" + col.getName() + ']',
-                    IgniteQueryErrorCode.UNSUPPORTED_OPERATION);
-
-            if (col.getDefaultExpression() != null)
-                throw new IgniteSQLException("DEFAULT expressions are not supported [colName=" + col.getName() + ']',
-                    IgniteQueryErrorCode.UNSUPPORTED_OPERATION);
-
-            if (col.getSequence() != null)
-                throw new IgniteSQLException("SEQUENCE columns are not supported [colName=" + col.getName() + ']',
-                    IgniteQueryErrorCode.UNSUPPORTED_OPERATION);
-
-            if (col.getSelectivity() != Constants.SELECTIVITY_DEFAULT)
-                throw new IgniteSQLException("SELECTIVITY column attr is not supported [colName=" + col.getName() + ']',
-                    IgniteQueryErrorCode.UNSUPPORTED_OPERATION);
-
-            if (COLUMN_CHECK_CONSTRAINT.get(col) != null)
-                throw new IgniteSQLException("Column CHECK constraints are not supported [colName=" + col.getName() +
-                    ']', IgniteQueryErrorCode.UNSUPPORTED_OPERATION);
-
-            GridSqlColumn gridCol = new GridSqlColumn(col, null, col.getName());
-
-            gridCol.resultType(GridSqlType.fromColumn(col));
-
-            cols.put(col.getName(), gridCol);
-        }
+        for (Column col : data.columns)
+            cols.put(col.getName(), parseColumn(col));
 
         if (cols.containsKey(QueryUtils.KEY_FIELD_NAME.toUpperCase()) ||
             cols.containsKey(QueryUtils.VAL_FIELD_NAME.toUpperCase()))
@@ -1009,6 +1002,98 @@ public class GridSqlQueryParser {
     }
 
     /**
+     * Parse {@code ALTER TABLE} statement.
+     * @param stmt H2 statement.
+     */
+    private GridSqlStatement parseAlterColumn(AlterTableAlterColumn stmt) {
+        switch (stmt.getType()) {
+            case CommandInterface.ALTER_TABLE_ADD_COLUMN:
+                return parseAddColumn(stmt);
+
+            default:
+                throw new IgniteSQLException("Unsupported operation code: " + stmt.getType());
+        }
+    }
+
+    /**
+     * Turn H2 column to grid column and check requested features.
+     * @param col H2 column.
+     * @return Grid column.
+     */
+    private static GridSqlColumn parseColumn(Column col) {
+        if (col.isAutoIncrement())
+            throw new IgniteSQLException("AUTO_INCREMENT columns are not supported [colName=" + col.getName() + ']',
+                IgniteQueryErrorCode.UNSUPPORTED_OPERATION);
+
+        if (!col.isNullable())
+            throw new IgniteSQLException("Non nullable columns are not supported [colName=" + col.getName() + ']',
+                IgniteQueryErrorCode.UNSUPPORTED_OPERATION);
+
+        if (COLUMN_IS_COMPUTED.get(col))
+            throw new IgniteSQLException("Computed columns are not supported [colName=" + col.getName() + ']',
+                IgniteQueryErrorCode.UNSUPPORTED_OPERATION);
+
+        if (col.getDefaultExpression() != null)
+            throw new IgniteSQLException("DEFAULT expressions are not supported [colName=" + col.getName() + ']',
+                IgniteQueryErrorCode.UNSUPPORTED_OPERATION);
+
+        if (col.getSequence() != null)
+            throw new IgniteSQLException("SEQUENCE columns are not supported [colName=" + col.getName() + ']',
+                IgniteQueryErrorCode.UNSUPPORTED_OPERATION);
+
+        if (col.getSelectivity() != Constants.SELECTIVITY_DEFAULT)
+            throw new IgniteSQLException("SELECTIVITY column attribute is not supported [colName=" + col.getName() + ']',
+                IgniteQueryErrorCode.UNSUPPORTED_OPERATION);
+
+        if (COLUMN_CHECK_CONSTRAINT.get(col) != null)
+            throw new IgniteSQLException("Column CHECK constraints are not supported [colName=" + col.getName() +
+                ']', IgniteQueryErrorCode.UNSUPPORTED_OPERATION);
+
+        GridSqlColumn gridCol = new GridSqlColumn(col, null, col.getName());
+
+        gridCol.resultType(GridSqlType.fromColumn(col));
+
+        return gridCol;
+    }
+
+    /**
+     * Parse {@code ALTER TABLE ... ADD COLUMN} statement.
+     * @param addCol H2 statement.
+     * @see <a href="http://www.h2database.com/html/grammar.html#alter_table_add"></a>
+     */
+    private GridSqlStatement parseAddColumn(AlterTableAlterColumn addCol) {
+        assert addCol.getType() == CommandInterface.ALTER_TABLE_ADD_COLUMN;
+
+        if (ALTER_COLUMN_BEFORE_COL.get(addCol) != null || ALTER_COLUMN_AFTER_COL.get(addCol) != null)
+            throw new IgniteSQLException("ALTER TABLE ADD COLUMN BEFORE/AFTER is not supported",
+                IgniteQueryErrorCode.UNSUPPORTED_OPERATION);
+
+        GridSqlAlterTableAddColumn res = new GridSqlAlterTableAddColumn();
+
+        ArrayList<Column> h2NewCols = ALTER_COLUMN_NEW_COLS.get(addCol);
+
+        GridSqlColumn[] gridNewCols = new GridSqlColumn[h2NewCols.size()];
+
+        for (int i = 0; i < h2NewCols.size(); i++)
+            gridNewCols[i] = parseColumn(h2NewCols.get(i));
+
+        res.columns(gridNewCols);
+
+        if (gridNewCols.length == 1)
+            res.ifNotExists(ALTER_COLUMN_IF_NOT_EXISTS.get(addCol));
+
+        res.ifTableExists(ALTER_COLUMN_IF_TBL_EXISTS.get(addCol));
+
+        Schema schema = SCHEMA_COMMAND_SCHEMA.get(addCol);
+
+        res.schemaName(schema.getName());
+
+        res.tableName(ALTER_COLUMN_TBL_NAME.get(addCol));
+
+        return res;
+    }
+
+    /**
      * @param name Param name.
      * @param val Param value.
      * @param res Table params to update.
@@ -1206,7 +1291,10 @@ public class GridSqlQueryParser {
             return parseCreateTable((CreateTable)stmt);
 
         if (stmt instanceof DropTable)
-            return parseDropTable((DropTable) stmt);
+            return parseDropTable((DropTable)stmt);
+
+        if (stmt instanceof AlterTableAlterColumn)
+            return parseAlterColumn((AlterTableAlterColumn)stmt);
 
         throw new CacheException("Unsupported SQL statement: " + stmt);
     }