You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cayenne.apache.org by aa...@apache.org on 2016/12/14 19:12:41 UTC

[05/16] cayenne git commit: New DbMerger for dbsync utils - merger process split in independent steps - new attributes compared (full type comparision of DbAttribute)

New DbMerger for dbsync utils
 - merger process split in independent steps
 - new attributes compared (full type comparision of DbAttribute)

First draft: no source filtering


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

Branch: refs/heads/master
Commit: 46c8ded55629f1a91764491e6a9a255bd0ae783f
Parents: 1e01dc8
Author: Nikita Timofeev <st...@gmail.com>
Authored: Fri Dec 9 18:17:39 2016 +0300
Committer: Nikita Timofeev <st...@gmail.com>
Committed: Fri Dec 9 18:17:40 2016 +0300

----------------------------------------------------------------------
 .../cayenne/dbsync/merge/AbstractMerger.java    |  89 ++++
 .../cayenne/dbsync/merge/AbstractToDbToken.java | 130 -----
 .../dbsync/merge/AbstractToModelToken.java      | 125 -----
 .../cayenne/dbsync/merge/AddColumnToDb.java     |  74 ---
 .../cayenne/dbsync/merge/AddColumnToModel.java  |  52 --
 .../dbsync/merge/AddRelationshipToDb.java       |  87 ----
 .../dbsync/merge/AddRelationshipToModel.java    |  89 ----
 .../cayenne/dbsync/merge/ChainMerger.java       |  64 +++
 .../cayenne/dbsync/merge/CreateTableToDb.java   |  86 ----
 .../dbsync/merge/CreateTableToModel.java        |  98 ----
 .../cayenne/dbsync/merge/DataMapMerger.java     | 144 ++++++
 .../dbsync/merge/DbAttributeDictionary.java     |  44 ++
 .../cayenne/dbsync/merge/DbAttributeMerger.java | 159 ++++++
 .../dbsync/merge/DbEntityDictionary.java        |  44 ++
 .../cayenne/dbsync/merge/DbEntityMerger.java    | 108 ++++
 .../apache/cayenne/dbsync/merge/DbMerger.java   |  12 +-
 .../dbsync/merge/DbRelationshipDictionary.java  |  83 +++
 .../dbsync/merge/DbRelationshipMerger.java      | 127 +++++
 .../dbsync/merge/DefaultModelMergeDelegate.java |  89 ----
 .../merge/DefaultValueForNullProvider.java      |  62 ---
 .../cayenne/dbsync/merge/DropColumnToDb.java    |  61 ---
 .../cayenne/dbsync/merge/DropColumnToModel.java |  77 ---
 .../dbsync/merge/DropRelationshipToDb.java      |  83 ---
 .../dbsync/merge/DropRelationshipToModel.java   |  49 --
 .../cayenne/dbsync/merge/DropTableToDb.java     |  51 --
 .../cayenne/dbsync/merge/DropTableToModel.java  |  51 --
 .../cayenne/dbsync/merge/DummyReverseToken.java |  65 ---
 .../dbsync/merge/EmptyValueForNullProvider.java |  40 --
 .../dbsync/merge/EntityMergeSupport.java        | 515 ------------------
 .../cayenne/dbsync/merge/MergeDirection.java    |  70 ---
 .../org/apache/cayenne/dbsync/merge/Merger.java |  30 ++
 .../cayenne/dbsync/merge/MergerContext.java     | 170 ------
 .../cayenne/dbsync/merge/MergerDictionary.java  |  54 ++
 .../dbsync/merge/MergerDictionaryDiff.java      | 117 +++++
 .../cayenne/dbsync/merge/MergerDiffPair.java    |  39 ++
 .../cayenne/dbsync/merge/MergerToken.java       |  57 --
 .../dbsync/merge/ModelMergeDelegate.java        |  62 ---
 .../dbsync/merge/ProxyModelMergeDelegate.java   | 108 ----
 .../cayenne/dbsync/merge/SetAllowNullToDb.java  |  58 ---
 .../dbsync/merge/SetAllowNullToModel.java       |  45 --
 .../cayenne/dbsync/merge/SetColumnTypeToDb.java | 111 ----
 .../dbsync/merge/SetColumnTypeToModel.java      |  95 ----
 .../cayenne/dbsync/merge/SetNotNullToDb.java    |  52 --
 .../cayenne/dbsync/merge/SetNotNullToModel.java |  45 --
 .../cayenne/dbsync/merge/SetPrimaryKeyToDb.java |  87 ----
 .../dbsync/merge/SetPrimaryKeyToModel.java      |  80 ---
 .../dbsync/merge/SetValueForNullToDb.java       |  48 --
 .../cayenne/dbsync/merge/TokenComparator.java   |  46 --
 .../dbsync/merge/ValueForNullProvider.java      |  41 --
 .../merge/context/EntityMergeSupport.java       | 516 +++++++++++++++++++
 .../dbsync/merge/context/MergeDirection.java    |  71 +++
 .../dbsync/merge/context/MergerContext.java     | 174 +++++++
 .../merge/factory/DB2MergerTokenFactory.java    |   4 +-
 .../factory/DefaultMergerTokenFactory.java      |  46 +-
 .../merge/factory/DerbyMergerTokenFactory.java  |   8 +-
 .../factory/FirebirdMergerTokenFactory.java     |  10 +-
 .../merge/factory/H2MergerTokenFactory.java     |   8 +-
 .../merge/factory/HSQLMergerTokenFactory.java   |   8 +-
 .../merge/factory/IngresMergerTokenFactory.java |  14 +-
 .../merge/factory/MergerTokenFactory.java       |   4 +-
 .../merge/factory/MySQLMergerTokenFactory.java  |  12 +-
 .../factory/OpenBaseMergerTokenFactory.java     |  12 +-
 .../merge/factory/OracleMergerTokenFactory.java |  10 +-
 .../factory/PostgresMergerTokenFactory.java     |   4 +-
 .../factory/SQLServerMergerTokenFactory.java    |  10 +-
 .../merge/factory/SybaseMergerTokenFactory.java |  12 +-
 .../dbsync/merge/token/AbstractToDbToken.java   | 133 +++++
 .../merge/token/AbstractToModelToken.java       | 128 +++++
 .../dbsync/merge/token/AddColumnToDb.java       |  74 +++
 .../dbsync/merge/token/AddColumnToModel.java    |  55 ++
 .../dbsync/merge/token/AddRelationshipToDb.java |  88 ++++
 .../merge/token/AddRelationshipToModel.java     |  91 ++++
 .../dbsync/merge/token/CreateTableToDb.java     |  88 ++++
 .../dbsync/merge/token/CreateTableToModel.java  | 100 ++++
 .../token/DefaultValueForNullProvider.java      |  63 +++
 .../dbsync/merge/token/DropColumnToDb.java      |  62 +++
 .../dbsync/merge/token/DropColumnToModel.java   |  79 +++
 .../merge/token/DropRelationshipToDb.java       |  84 +++
 .../merge/token/DropRelationshipToModel.java    |  51 ++
 .../dbsync/merge/token/DropTableToDb.java       |  52 ++
 .../dbsync/merge/token/DropTableToModel.java    |  53 ++
 .../dbsync/merge/token/DummyReverseToken.java   |  68 +++
 .../merge/token/EmptyValueForNullProvider.java  |  41 ++
 .../cayenne/dbsync/merge/token/MergerToken.java |  58 +++
 .../dbsync/merge/token/SetAllowNullToDb.java    |  59 +++
 .../dbsync/merge/token/SetAllowNullToModel.java |  47 ++
 .../dbsync/merge/token/SetColumnTypeToDb.java   | 112 ++++
 .../merge/token/SetColumnTypeToModel.java       |  97 ++++
 .../dbsync/merge/token/SetNotNullToDb.java      |  53 ++
 .../dbsync/merge/token/SetNotNullToModel.java   |  47 ++
 .../dbsync/merge/token/SetPrimaryKeyToDb.java   |  88 ++++
 .../merge/token/SetPrimaryKeyToModel.java       |  82 +++
 .../dbsync/merge/token/SetValueForNullToDb.java |  49 ++
 .../dbsync/merge/token/TokenComparator.java     |  46 ++
 .../merge/token/ValueForNullProvider.java       |  42 ++
 .../dbload/DefaultModelMergeDelegate.java       |  90 ++++
 .../reverse/dbload/ModelMergeDelegate.java      |  65 +++
 .../reverse/dbload/ProxyModelMergeDelegate.java | 109 ++++
 .../dbsync/merge/AddColumnToModelIT.java        |   2 +
 .../dbsync/merge/CreateTableToModelIT.java      |   3 +
 .../cayenne/dbsync/merge/DbMergerTest.java      |  35 +-
 .../dbsync/merge/DropColumnToModelIT.java       |   3 +
 .../dbsync/merge/DropRelationshipToModelIT.java |   3 +
 .../dbsync/merge/DropTableToModelIT.java        |   2 +
 .../dbsync/merge/EntityMergeSupportIT.java      |   1 +
 .../apache/cayenne/dbsync/merge/MergeCase.java  |   3 +
 .../dbsync/merge/TokenComparatorTest.java       |  14 +
 .../cayenne/dbsync/merge/TokensReverseTest.java |   3 +-
 .../merge/TokensToModelExecutionTest.java       |   1 +
 .../cayenne/dbsync/merge/ValueForNullIT.java    |   1 +
 .../java/org/apache/cayenne/map/DbEntity.java   |   4 +-
 .../tools/dbimport/DbImportConfiguration.java   |   4 +-
 .../tools/dbimport/DefaultDbImportAction.java   |  16 +-
 .../dbimport/DefaultDbImportActionTest.java     |  12 +-
 .../modeler/action/CreateObjEntityAction.java   |   2 +-
 .../modeler/action/DbEntitySyncAction.java      |   2 +-
 .../modeler/action/ObjEntitySyncAction.java     |   2 +-
 .../modeler/dialog/db/MergerOptions.java        |  18 +-
 .../db/MergerTokenSelectorController.java       |   6 +-
 .../dialog/db/MergerTokenTableModel.java        |   4 +-
 .../dialog/objentity/EntitySyncController.java  |   2 +-
 121 files changed, 4398 insertions(+), 3195 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cayenne/blob/46c8ded5/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/AbstractMerger.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/AbstractMerger.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/AbstractMerger.java
new file mode 100644
index 0000000..c6772e1
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/AbstractMerger.java
@@ -0,0 +1,89 @@
+/*****************************************************************
+ *   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.cayenne.dbsync.merge;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.apache.cayenne.dbsync.merge.factory.MergerTokenFactory;
+import org.apache.cayenne.dbsync.merge.token.MergerToken;
+import org.apache.cayenne.map.DataMap;
+
+abstract class AbstractMerger<T, M> implements Merger<T> {
+
+    private MergerDictionaryDiff<M> diff;
+    private MergerTokenFactory tokenFactory;
+    DataMap originalDataMap;
+    DataMap importedDataMap;
+
+    AbstractMerger(MergerTokenFactory tokenFactory, DataMap original, DataMap imported) {
+        this.tokenFactory = tokenFactory;
+        this.originalDataMap = original;
+        this.importedDataMap = imported;
+    }
+
+    public List<MergerToken> createMergeTokens(T original, T imported) {
+        diff = createDiff(original, imported);
+
+        List<MergerToken> tokens = new ArrayList<>();
+
+        for(MergerDiffPair<M> pair : diff.getMissing()) {
+            Collection<MergerToken> tokensForMissing = createTokensForMissing(pair);
+            if(tokensForMissing != null) {
+                tokens.addAll(tokensForMissing);
+            }
+        }
+
+        for(MergerDiffPair<M> pair : diff.getSame()) {
+            Collection<MergerToken> tokensForSame = createTokensForSame(pair);
+            if(tokensForSame != null) {
+                tokens.addAll(tokensForSame);
+            }
+        }
+
+        return tokens;
+    }
+
+    MergerDictionaryDiff<M> getDiff() {
+        return diff;
+    }
+
+    MergerTokenFactory getTokenFactory() {
+        return tokenFactory;
+    }
+
+    private Collection<MergerToken> createTokensForMissing(MergerDiffPair<M> missing) {
+        if(missing.getOriginal() == null) {
+            return createTokensForMissingOriginal(missing.getImported());
+        } else {
+            return createTokensForMissingImported(missing.getOriginal());
+        }
+    }
+
+    abstract MergerDictionaryDiff<M> createDiff(T original, T imported);
+
+    abstract Collection<MergerToken> createTokensForMissingOriginal(M imported);
+
+    abstract Collection<MergerToken> createTokensForMissingImported(M original);
+
+    abstract Collection<MergerToken> createTokensForSame(MergerDiffPair<M> same);
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/46c8ded5/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/AbstractToDbToken.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/AbstractToDbToken.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/AbstractToDbToken.java
deleted file mode 100644
index 88e056f..0000000
--- a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/AbstractToDbToken.java
+++ /dev/null
@@ -1,130 +0,0 @@
-/*****************************************************************
- *   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.cayenne.dbsync.merge;
-
-import org.apache.cayenne.dba.DbAdapter;
-import org.apache.cayenne.log.JdbcEventLogger;
-import org.apache.cayenne.map.DbAttribute;
-import org.apache.cayenne.map.DbEntity;
-import org.apache.cayenne.validation.SimpleValidationFailure;
-
-import java.sql.Connection;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.List;
-
-/**
- * Common abstract superclass for all {@link MergerToken}s going from the model
- * to the database.
- */
-public abstract class AbstractToDbToken implements MergerToken, Comparable<MergerToken> {
-
-	private final String tokenName;
-
-	protected AbstractToDbToken(String tokenName) {
-		this.tokenName = tokenName;
-	}
-
-	@Override
-	public final String getTokenName() {
-		return tokenName;
-	}
-
-	@Override
-	public final MergeDirection getDirection() {
-		return MergeDirection.TO_DB;
-	}
-
-	@Override
-	public void execute(MergerContext mergerContext) {
-		for (String sql : createSql(mergerContext.getDataNode().getAdapter())) {
-			executeSql(mergerContext, sql);
-		}
-	}
-
-	protected void executeSql(MergerContext mergerContext, String sql) {
-		JdbcEventLogger logger = mergerContext.getDataNode().getJdbcEventLogger();
-		logger.log(sql);
-
-		try (Connection conn = mergerContext.getDataNode().getDataSource().getConnection();) {
-
-			try (Statement st = conn.createStatement();) {
-				st.execute(sql);
-			}
-		} catch (SQLException e) {
-			mergerContext.getValidationResult().addFailure(new SimpleValidationFailure(sql, e.getMessage()));
-			logger.logQueryError(e);
-		}
-	}
-
-	@Override
-	public String toString() {
-		return getTokenName() + ' ' + getTokenValue() + ' ' + getDirection();
-	}
-
-	public boolean isEmpty() {
-		return false;
-	}
-
-	public abstract List<String> createSql(DbAdapter adapter);
-
-	abstract static class Entity extends AbstractToDbToken {
-
-		private DbEntity entity;
-
-		public Entity(String tokenName, DbEntity entity) {
-			super(tokenName);
-			this.entity = entity;
-		}
-
-		public DbEntity getEntity() {
-			return entity;
-		}
-
-		public String getTokenValue() {
-			return getEntity().getName();
-		}
-
-		public int compareTo(MergerToken o) {
-			// default order as tokens are created
-			return 0;
-		}
-
-	}
-
-	abstract static class EntityAndColumn extends Entity {
-
-		private DbAttribute column;
-
-		public EntityAndColumn(String tokenName, DbEntity entity, DbAttribute column) {
-			super(tokenName, entity);
-			this.column = column;
-		}
-
-		public DbAttribute getColumn() {
-			return column;
-		}
-
-		@Override
-		public String getTokenValue() {
-			return getEntity().getName() + "." + getColumn().getName();
-		}
-
-	}
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/46c8ded5/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/AbstractToModelToken.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/AbstractToModelToken.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/AbstractToModelToken.java
deleted file mode 100644
index 24cee4a..0000000
--- a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/AbstractToModelToken.java
+++ /dev/null
@@ -1,125 +0,0 @@
-/*****************************************************************
- *   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.cayenne.dbsync.merge;
-
-import org.apache.cayenne.map.DbAttribute;
-import org.apache.cayenne.map.DbEntity;
-import org.apache.cayenne.map.DbRelationship;
-import org.apache.cayenne.map.ObjEntity;
-import org.apache.cayenne.map.ObjRelationship;
-
-/**
- * Common abstract superclass for all {@link MergerToken}s going from the database to the
- * model.
- */
-public abstract class AbstractToModelToken implements MergerToken {
-
-    private final String tokenName;
-
-    protected AbstractToModelToken(String tokenName) {
-        this.tokenName = tokenName;
-    }
-
-    protected static void remove(ModelMergeDelegate mergerContext, DbRelationship rel, boolean reverse) {
-        if (rel == null) {
-            return;
-        }
-        if (reverse) {
-            remove(mergerContext, rel.getReverseRelationship(), false);
-        }
-
-        DbEntity dbEntity = rel.getSourceEntity();
-        for (ObjEntity objEntity : dbEntity.mappedObjEntities()) {
-            remove(mergerContext, objEntity.getRelationshipForDbRelationship(rel), true);
-        }
-
-        rel.getSourceEntity().removeRelationship(rel.getName());
-        mergerContext.dbRelationshipRemoved(rel);
-    }
-
-    protected static void remove(ModelMergeDelegate mergerContext, ObjRelationship rel, boolean reverse) {
-        if (rel == null) {
-            return;
-        }
-        if (reverse) {
-            remove(mergerContext, rel.getReverseRelationship(), false);
-        }
-        rel.getSourceEntity().removeRelationship(rel.getName());
-        mergerContext.objRelationshipRemoved(rel);
-    }
-
-    @Override
-    public final String getTokenName() {
-        return tokenName;
-    }
-
-    @Override
-    public final MergeDirection getDirection() {
-        return MergeDirection.TO_MODEL;
-    }
-
-    @Override
-    public String toString() {
-        return getTokenName() + ' ' + getTokenValue() + ' ' + getDirection();
-    }
-
-    public boolean isEmpty() {
-        return false;
-    }
-
-    abstract static class Entity extends AbstractToModelToken {
-
-        private final DbEntity entity;
-
-        protected Entity(String tokenName, DbEntity entity) {
-            super(tokenName);
-            this.entity = entity;
-        }
-
-        public DbEntity getEntity() {
-            return entity;
-        }
-
-        public String getTokenValue() {
-            return getEntity().getName();
-        }
-
-    }
-
-    abstract static class EntityAndColumn extends Entity {
-
-        private final DbAttribute column;
-
-        protected EntityAndColumn(String tokenName, DbEntity entity, DbAttribute column) {
-            super(tokenName, entity);
-            this.column = column;
-        }
-
-        public DbAttribute getColumn() {
-            return column;
-        }
-
-        @Override
-        public String getTokenValue() {
-            return getEntity().getName() + "." + getColumn().getName();
-        }
-
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/46c8ded5/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/AddColumnToDb.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/AddColumnToDb.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/AddColumnToDb.java
deleted file mode 100644
index 6fdca2f..0000000
--- a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/AddColumnToDb.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * 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.cayenne.dbsync.merge;
-
-import org.apache.cayenne.dba.DbAdapter;
-import org.apache.cayenne.dba.JdbcAdapter;
-import org.apache.cayenne.dba.QuotingStrategy;
-import org.apache.cayenne.dbsync.merge.factory.MergerTokenFactory;
-import org.apache.cayenne.map.DbAttribute;
-import org.apache.cayenne.map.DbEntity;
-
-import java.util.Collections;
-import java.util.List;
-
-public class AddColumnToDb extends AbstractToDbToken.EntityAndColumn {
-
-    public AddColumnToDb(DbEntity entity, DbAttribute column) {
-        super("Add Column", entity, column);
-    }
-
-    /**
-     * append the part of the token before the actual column data type
-     */
-    protected void appendPrefix(StringBuffer sqlBuffer, QuotingStrategy context) {
-
-        sqlBuffer.append("ALTER TABLE ");
-        sqlBuffer.append(context.quotedFullyQualifiedName(getEntity()));
-        sqlBuffer.append(" ADD COLUMN ");
-        sqlBuffer.append(context.quotedName(getColumn()));
-        sqlBuffer.append(" ");
-    }
-
-    @Override
-    public List<String> createSql(DbAdapter adapter) {
-        StringBuffer sqlBuffer = new StringBuffer();
-        QuotingStrategy context = adapter.getQuotingStrategy();
-        appendPrefix(sqlBuffer, context);
-
-        sqlBuffer.append(JdbcAdapter.getType(adapter, getColumn()));
-        sqlBuffer.append(JdbcAdapter.sizeAndPrecision(adapter, getColumn()));
-
-        return Collections.singletonList(sqlBuffer.toString());
-    }
-
-    @Override
-    public MergerToken createReverse(MergerTokenFactory factory) {
-        return factory.createDropColumnToModel(getEntity(), getColumn());
-    }
-
-    @Override
-    public int compareTo(MergerToken o) {
-        // add all AddRelationshipToDb to the end.
-        if (o instanceof AddRelationshipToDb) {
-            return -1;
-        }
-        return super.compareTo(o);
-    }
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/46c8ded5/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/AddColumnToModel.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/AddColumnToModel.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/AddColumnToModel.java
deleted file mode 100644
index 823a81d..0000000
--- a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/AddColumnToModel.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*****************************************************************
- *   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.cayenne.dbsync.merge;
-
-import org.apache.cayenne.dbsync.merge.factory.MergerTokenFactory;
-import org.apache.cayenne.map.DbAttribute;
-import org.apache.cayenne.map.DbEntity;
-import org.apache.cayenne.map.ObjEntity;
-
-/**
- * A {@link MergerToken} to add a {@link DbAttribute} to a {@link DbEntity}. The
- * {@link EntityMergeSupport} will be used to update the mapped {@link ObjEntity}
- */
-public class AddColumnToModel extends AbstractToModelToken.EntityAndColumn {
-
-    public AddColumnToModel(DbEntity entity, DbAttribute column) {
-        super("Add Column", entity, column);
-    }
-
-    @Override
-    public MergerToken createReverse(MergerTokenFactory factory) {
-        return factory.createDropColumnToDb(getEntity(), getColumn());
-    }
-
-    @Override
-    public void execute(MergerContext mergerContext) {
-        getEntity().addAttribute(getColumn());
-
-        for (ObjEntity e : getEntity().mappedObjEntities()) {
-            mergerContext.getEntityMergeSupport().synchronizeOnDbAttributeAdded(e, getColumn());
-        }
-
-        mergerContext.getDelegate().dbAttributeAdded(getColumn());
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/46c8ded5/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/AddRelationshipToDb.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/AddRelationshipToDb.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/AddRelationshipToDb.java
deleted file mode 100644
index 8712613..0000000
--- a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/AddRelationshipToDb.java
+++ /dev/null
@@ -1,87 +0,0 @@
-/*****************************************************************
- *   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.cayenne.dbsync.merge;
-
-import org.apache.cayenne.access.DbGenerator;
-import org.apache.cayenne.dba.DbAdapter;
-import org.apache.cayenne.dbsync.merge.factory.MergerTokenFactory;
-import org.apache.cayenne.map.DbEntity;
-import org.apache.cayenne.map.DbRelationship;
-
-import java.util.Collections;
-import java.util.List;
-
-public class AddRelationshipToDb extends AbstractToDbToken.Entity {
-
-    private DbRelationship relationship;
-
-    public AddRelationshipToDb(DbEntity entity, DbRelationship relationship) {
-        super("Add foreign key", entity);
-        this.relationship = relationship;
-    }
-
-    /**
-     * @see DbGenerator#createConstraintsQueries(org.apache.cayenne.map.DbEntity)
-     */
-    @Override
-    public List<String> createSql(DbAdapter adapter) {
-        // TODO: skip FK to a different DB
-        if (!this.isEmpty()) {
-            String fksql = adapter.createFkConstraint(relationship);
-            if (fksql != null) {
-                return Collections.singletonList(fksql);
-            }
-        }
-        return Collections.emptyList();
-    }
-
-    @Override
-    public MergerToken createReverse(MergerTokenFactory factory) {
-        return factory.createDropRelationshipToModel(getEntity(), relationship);
-    }
-
-    @Override
-    public String getTokenValue() {
-        if (!this.isEmpty()) {
-            return relationship.getSourceEntity().getName() + "->" + relationship.getTargetEntityName();
-        } else {
-            return "Skip. No sql representation.";
-        }
-    }
-
-    @Override
-    public boolean isEmpty() {
-        // Method DbRelationship.isSourceIndependentFromTargetChange() looks same
-        return relationship.isSourceIndependentFromTargetChange();
-        /*return relationship.isToMany()
-                || relationship.isToDependentPK()
-                || !relationship.isToPK(); // TODO it is not necessary primary key it can be unique index
-        */
-    }
-
-    @Override
-    public int compareTo(MergerToken o) {
-        // add all AddRelationshipToDb to the end.
-        if (o instanceof AddRelationshipToDb) {
-            return super.compareTo(o);
-        }
-        return 1;
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/46c8ded5/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/AddRelationshipToModel.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/AddRelationshipToModel.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/AddRelationshipToModel.java
deleted file mode 100644
index 05a3d6c..0000000
--- a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/AddRelationshipToModel.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*****************************************************************
- *   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.cayenne.dbsync.merge;
-
-import org.apache.cayenne.dbsync.merge.factory.MergerTokenFactory;
-import org.apache.cayenne.map.DbEntity;
-import org.apache.cayenne.map.DbJoin;
-import org.apache.cayenne.map.DbRelationship;
-import org.apache.cayenne.map.ObjEntity;
-
-public class AddRelationshipToModel extends AbstractToModelToken.Entity {
-
-    public static final String COMMA_SEPARATOR = ", ";
-    public static final int COMMA_SEPARATOR_LENGTH = COMMA_SEPARATOR.length();
-
-    private DbRelationship relationship;
-
-    public AddRelationshipToModel(DbEntity entity, DbRelationship relationship) {
-        super("Add Relationship", entity);
-        this.relationship = relationship;
-    }
-
-    public static String getTokenValue(DbRelationship rel) {
-        String attributes = "";
-        if (rel.getJoins().size() == 1) {
-            attributes = rel.getJoins().get(0).getTargetName();
-        } else {
-            for (DbJoin dbJoin : rel.getJoins()) {
-                attributes += dbJoin.getTargetName() + COMMA_SEPARATOR;
-            }
-
-            attributes = "{" + attributes.substring(0, attributes.length() - COMMA_SEPARATOR_LENGTH) + "}";
-        }
-
-        return rel.getName() + " " + rel.getSourceEntity().getName() + "->" + rel.getTargetEntityName() + "." + attributes;
-    }
-
-    @Override
-    public MergerToken createReverse(MergerTokenFactory factory) {
-        return factory.createDropRelationshipToDb(getEntity(), relationship);
-    }
-
-    @Override
-    public void execute(MergerContext context) {
-        // Set name to relationship if it was created without it, e.g. in createReverse() action
-        if(relationship.getName() == null) {
-            relationship.setName(context.getNameGenerator().relationshipName(relationship));
-        }
-
-        getEntity().addRelationship(relationship);
-        for (ObjEntity e : getEntity().mappedObjEntities()) {
-            context.getEntityMergeSupport().synchronizeOnDbRelationshipAdded(e, relationship);
-        }
-
-        context.getDelegate().dbRelationshipAdded(relationship);
-    }
-
-    @Override
-    public String getTokenValue() {
-        String attributes = "";
-        if (relationship.getJoins().size() == 1) {
-            attributes = relationship.getJoins().get(0).getTargetName();
-        } else {
-            for (DbJoin dbJoin : relationship.getJoins()) {
-                attributes += dbJoin.getTargetName() + COMMA_SEPARATOR;
-            }
-
-            attributes = "{" + attributes.substring(0, attributes.length() - COMMA_SEPARATOR_LENGTH) + "}";
-        }
-
-        return relationship.getName() + " " + relationship.getSourceEntity().getName() + "->" + relationship.getTargetEntityName() + "." + attributes;
-    }
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/46c8ded5/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/ChainMerger.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/ChainMerger.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/ChainMerger.java
new file mode 100644
index 0000000..04583d6
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/ChainMerger.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.cayenne.dbsync.merge;
+
+import java.util.Collection;
+
+import org.apache.cayenne.dbsync.merge.factory.MergerTokenFactory;
+import org.apache.cayenne.dbsync.merge.token.MergerToken;
+import org.apache.cayenne.map.DataMap;
+
+class ChainMerger<T, M> extends AbstractMerger<T, M> {
+
+    private final AbstractMerger<T, M> merger;
+
+    private final AbstractMerger<?, T> parentMerger;
+
+    ChainMerger(MergerTokenFactory tokenFactory, DataMap original, DataMap imported, AbstractMerger<T, M> merger, AbstractMerger<?, T> parentMerger) {
+        super(tokenFactory, original, imported);
+        this.merger = merger;
+        this.parentMerger = parentMerger;
+    }
+
+    @Override
+    MergerDictionaryDiff<M> createDiff(T original, T imported) {
+        MergerDictionaryDiff<M> diff = new MergerDictionaryDiff<M>();
+        MergerDictionaryDiff<T> parentDiff = parentMerger.getDiff();
+        for(MergerDiffPair<T> pair : parentDiff.getSame()) {
+            diff.addAll(merger.createDiff(pair.getOriginal(), pair.getImported()));
+        }
+        return diff;
+    }
+
+    @Override
+    Collection<MergerToken> createTokensForMissingOriginal(M imported) {
+        return merger.createTokensForMissingOriginal(imported);
+    }
+
+    @Override
+    Collection<MergerToken> createTokensForMissingImported(M original) {
+        return merger.createTokensForMissingImported(original);
+    }
+
+    @Override
+    Collection<MergerToken> createTokensForSame(MergerDiffPair<M> same) {
+        return merger.createTokensForSame(same);
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/46c8ded5/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/CreateTableToDb.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/CreateTableToDb.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/CreateTableToDb.java
deleted file mode 100644
index b5d920c..0000000
--- a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/CreateTableToDb.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*****************************************************************
- *   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.cayenne.dbsync.merge;
-
-import org.apache.cayenne.access.DataNode;
-import org.apache.cayenne.dba.DbAdapter;
-import org.apache.cayenne.dbsync.merge.factory.MergerTokenFactory;
-import org.apache.cayenne.map.DbAttribute;
-import org.apache.cayenne.map.DbEntity;
-import org.apache.cayenne.validation.SimpleValidationFailure;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-public class CreateTableToDb extends AbstractToDbToken.Entity {
-
-    public CreateTableToDb(DbEntity entity) {
-        super("Create Table", entity);
-    }
-
-    @Override
-    public List<String> createSql(DbAdapter adapter) {
-        List<String> sqls = new ArrayList<String>();
-        if(needAutoPkSupport()) {
-            sqls.addAll(adapter.getPkGenerator().createAutoPkStatements(
-                    Collections.singletonList(getEntity())));
-        }
-        sqls.add(adapter.createTable(getEntity()));
-        return sqls;
-    }
-
-    @Override
-    public void execute(MergerContext mergerContext) {
-        try {
-            DataNode node = mergerContext.getDataNode();
-            DbAdapter adapter = node.getAdapter();
-            if(needAutoPkSupport()) {
-                adapter.getPkGenerator().createAutoPk(
-                        node,
-                        Collections.singletonList(getEntity()));
-            }
-            executeSql(mergerContext, adapter.createTable(getEntity()));
-        }
-        catch (Exception e) {
-            mergerContext.getValidationResult().addFailure(
-                    new SimpleValidationFailure(this, e.getMessage()));
-        }
-    }
-
-    private boolean needAutoPkSupport() {
-        DbEntity entity = getEntity();
-        if(entity.getPrimaryKeyGenerator() != null) {
-            return false;
-        }
-
-        for(DbAttribute attribute : entity.getPrimaryKeys()) {
-            if(attribute.isGenerated()) {
-                return false;
-            }
-        }
-
-        return true;
-    }
-
-    public MergerToken createReverse(MergerTokenFactory factory) {
-        return factory.createDropTableToModel(getEntity());
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/46c8ded5/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/CreateTableToModel.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/CreateTableToModel.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/CreateTableToModel.java
deleted file mode 100644
index 8352a27..0000000
--- a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/CreateTableToModel.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/*****************************************************************
- *   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.cayenne.dbsync.merge;
-
-import org.apache.cayenne.dbsync.merge.factory.MergerTokenFactory;
-import org.apache.cayenne.dbsync.naming.NameBuilder;
-import org.apache.cayenne.map.DataMap;
-import org.apache.cayenne.map.DbEntity;
-import org.apache.cayenne.map.ObjEntity;
-
-/**
- * A {@link MergerToken} to add a {@link DbEntity} to a {@link DataMap}
- */
-public class CreateTableToModel extends AbstractToModelToken.Entity {
-
-    /**
-     * className if {@link ObjEntity} should be generated with a
-     * special class name.
-     * Setting this to <code>null</code>, because by default class name should be generated
-     */
-    private String objEntityClassName;
-
-    public CreateTableToModel(DbEntity entity) {
-        super("Create Table", entity);
-    }
-
-    /**
-     * Set the {@link ObjEntity} className if {@link ObjEntity} should be generated with a
-     * special class name. Set to null if the {@link ObjEntity} should be created with a
-     * name based on {@link DataMap#getDefaultPackage()} and {@link ObjEntity#getName()}
-     * <p>
-     * The default value is <code>null</code>
-     */
-    public void setObjEntityClassName(String n) {
-        objEntityClassName = n;
-    }
-
-    @Override
-    public void execute(MergerContext context) {
-        DbEntity dbEntity = getEntity();
-
-        DataMap map = context.getDataMap();
-        map.addDbEntity(dbEntity);
-
-        // create a ObjEntity
-        ObjEntity objEntity = new ObjEntity();
-
-        objEntity.setName(NameBuilder
-                .builder(objEntity, dbEntity.getDataMap())
-                .baseName(context.getNameGenerator().objEntityName(dbEntity))
-                .name());
-        objEntity.setDbEntity(getEntity());
-
-        // try to find a class name for the ObjEntity
-        String className = objEntityClassName;
-        if (className == null) {
-            // generate a className based on the objEntityName
-            className = map.getNameWithDefaultPackage(objEntity.getName());
-        }
-
-        objEntity.setClassName(className);
-        objEntity.setSuperClassName(map.getDefaultSuperclass());
-
-        if (map.isClientSupported()) {
-            objEntity.setClientClassName(map.getNameWithDefaultClientPackage(objEntity.getName()));
-            objEntity.setClientSuperClassName(map.getDefaultClientSuperclass());
-        }
-
-        map.addObjEntity(objEntity);
-
-        // presumably there are no other ObjEntities pointing to this DbEntity, so syncing just this one...
-        context.getEntityMergeSupport().synchronizeWithDbEntity(objEntity);
-
-        context.getDelegate().dbEntityAdded(getEntity());
-        context.getDelegate().objEntityAdded(objEntity);
-    }
-
-    public MergerToken createReverse(MergerTokenFactory factory) {
-        return factory.createDropTableToDb(getEntity());
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/46c8ded5/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DataMapMerger.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DataMapMerger.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DataMapMerger.java
new file mode 100644
index 0000000..51bea0d
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DataMapMerger.java
@@ -0,0 +1,144 @@
+/*****************************************************************
+ *   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.cayenne.dbsync.merge;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.cayenne.dbsync.merge.token.EmptyValueForNullProvider;
+import org.apache.cayenne.dbsync.merge.token.ValueForNullProvider;
+import org.apache.cayenne.dbsync.merge.factory.MergerTokenFactory;
+import org.apache.cayenne.dbsync.merge.token.MergerToken;
+import org.apache.cayenne.dbsync.merge.token.TokenComparator;
+import org.apache.cayenne.dbsync.reverse.filters.FiltersConfig;
+import org.apache.cayenne.dbsync.reverse.filters.PatternFilter;
+import org.apache.cayenne.dbsync.reverse.filters.TableFilter;
+import org.apache.cayenne.map.DataMap;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.DbRelationship;
+
+/**
+ * Synchronization data base store and Cayenne model.
+ */
+public class DataMapMerger implements Merger<DataMap> {
+
+    private MergerTokenFactory tokenFactory;
+    private ValueForNullProvider valueForNull;
+    private boolean skipRelationshipsTokens;
+    private boolean skipPKTokens;
+    private FiltersConfig filters;
+
+    private DataMapMerger() {
+    }
+
+    /**
+     * Create List of MergerToken that represent the difference between two {@link DataMap} objects.
+     */
+    public List<MergerToken> createMergeTokens(DataMap original, DataMap importedFromDb) {
+        List<MergerToken> tokens = new ArrayList<>();
+
+        DbEntityMerger dbEntityMerger = mergeDbEntities(tokens, original, importedFromDb);
+        mergeAttributes(tokens, dbEntityMerger, original, importedFromDb);
+        mergeRelationships(tokens, dbEntityMerger, original, importedFromDb);
+
+        Collections.sort(tokens, new TokenComparator());
+
+        return tokens;
+    }
+
+    private DbEntityMerger mergeDbEntities(List<MergerToken> tokens, DataMap original, DataMap importedFromDb) {
+        DbEntityMerger dbEntityMerger = new DbEntityMerger(tokenFactory, skipPKTokens, original, importedFromDb);
+        tokens.addAll(dbEntityMerger.createMergeTokens(original, importedFromDb));
+        return dbEntityMerger;
+    }
+
+    private void mergeAttributes(List<MergerToken> tokens, DbEntityMerger dbEntityMerger, DataMap original, DataMap importedFromDb) {
+        ChainMerger<DbEntity, DbAttribute> dbAttributeMerger = new ChainMerger<>(
+                tokenFactory, original, importedFromDb,
+                new DbAttributeMerger(tokenFactory, original, importedFromDb, valueForNull),
+                dbEntityMerger
+        );
+        tokens.addAll(dbAttributeMerger.createMergeTokens(null, null));
+    }
+
+    private void mergeRelationships(List<MergerToken> tokens, DbEntityMerger dbEntityMerger, DataMap original, DataMap importedFromDb) {
+        ChainMerger<DbEntity, DbRelationship> dbRelationshipMerger = new ChainMerger<>(
+                tokenFactory, original, importedFromDb,
+                new DbRelationshipMerger(tokenFactory, original, importedFromDb, skipRelationshipsTokens),
+                dbEntityMerger
+        );
+        tokens.addAll(dbRelationshipMerger.createMergeTokens(null, null));
+    }
+
+
+    public static Builder builder(MergerTokenFactory tokenFactory) {
+        return new Builder(tokenFactory);
+    }
+
+    public static DataMapMerger build(MergerTokenFactory tokenFactory) {
+        return builder(tokenFactory).build();
+    }
+
+    public static class Builder {
+        private DataMapMerger merger;
+
+        private Builder(MergerTokenFactory tokenFactory) {
+            this.merger = new DataMapMerger();
+            this.merger.tokenFactory = Objects.requireNonNull(tokenFactory);
+        }
+
+        public DataMapMerger build() {
+
+            if (merger.valueForNull == null) {
+                merger.valueForNull = new EmptyValueForNullProvider();
+            }
+
+            if (merger.filters == null) {
+                // default: match all tables, no stored procedures
+                merger.filters = FiltersConfig.create(null, null, TableFilter.everything(), PatternFilter.INCLUDE_NOTHING);
+            }
+
+            return merger;
+        }
+
+        public Builder valueForNullProvider(ValueForNullProvider provider) {
+            merger.valueForNull = provider;
+            return this;
+        }
+
+        public Builder skipRelationshipsTokens(boolean flag) {
+            merger.skipRelationshipsTokens = flag;
+            return this;
+        }
+
+        public Builder skipPKTokens(boolean flag) {
+            merger.skipPKTokens = flag;
+            return this;
+        }
+
+        public Builder filters(FiltersConfig filters) {
+            merger.filters = Objects.requireNonNull(filters);
+            return this;
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/46c8ded5/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DbAttributeDictionary.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DbAttributeDictionary.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DbAttributeDictionary.java
new file mode 100644
index 0000000..1fc17da
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DbAttributeDictionary.java
@@ -0,0 +1,44 @@
+/*****************************************************************
+ *   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.cayenne.dbsync.merge;
+
+import java.util.Collection;
+
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+
+class DbAttributeDictionary extends MergerDictionary<DbAttribute> {
+
+    private final DbEntity container;
+
+    DbAttributeDictionary(DbEntity container) {
+        this.container = container;
+    }
+
+    @Override
+    String getName(DbAttribute entity) {
+        return entity.getName();
+    }
+
+    @Override
+    Collection<DbAttribute> getAll() {
+        return container.getAttributes();
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/46c8ded5/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DbAttributeMerger.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DbAttributeMerger.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DbAttributeMerger.java
new file mode 100644
index 0000000..cf26201
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DbAttributeMerger.java
@@ -0,0 +1,159 @@
+/*****************************************************************
+ *   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.cayenne.dbsync.merge;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.apache.cayenne.dbsync.merge.token.ValueForNullProvider;
+import org.apache.cayenne.dbsync.merge.factory.MergerTokenFactory;
+import org.apache.cayenne.dbsync.merge.token.MergerToken;
+import org.apache.cayenne.map.DataMap;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+
+class DbAttributeMerger extends AbstractMerger<DbEntity, DbAttribute> {
+
+    private final ValueForNullProvider valueForNull;
+
+    DbAttributeMerger(MergerTokenFactory tokenFactory, DataMap original, DataMap imported, ValueForNullProvider valueForNull) {
+        super(tokenFactory, original, imported);
+        this.valueForNull = valueForNull;
+    }
+
+    @Override
+    MergerDictionaryDiff<DbAttribute> createDiff(DbEntity original, DbEntity imported) {
+        return new MergerDictionaryDiff.Builder<DbAttribute>()
+                .originalDictionary(new DbAttributeDictionary(original))
+                .importedDictionary(new DbAttributeDictionary(imported))
+                .build();
+    }
+
+    /**
+     * Add column to db
+     * @param original attribute found in model but missing in db
+     */
+    @Override
+    Collection<MergerToken> createTokensForMissingImported(DbAttribute original) {
+        DbEntity originalDbEntity = original.getEntity();
+        List<MergerToken> tokens = new LinkedList<>();
+        tokens.add(getTokenFactory().createAddColumnToDb(originalDbEntity, original));
+
+        // Create not null check
+        if (original.isMandatory()) {
+            if (valueForNull.hasValueFor(originalDbEntity, original)) {
+                tokens.add(getTokenFactory().createSetValueForNullToDb(originalDbEntity, original, valueForNull));
+            }
+            tokens.add(getTokenFactory().createSetNotNullToDb(originalDbEntity, original));
+        }
+
+        return tokens;
+    }
+
+    /**
+     * Drop column in db
+     * @param imported attribute found in db but missing in model
+     */
+    @Override
+    Collection<MergerToken> createTokensForMissingOriginal(DbAttribute imported) {
+        DbEntity originalDbEntity = originalDataMap.getDbEntity(imported.getEntity().getName());
+        return Collections.singleton(getTokenFactory().createDropColumnToDb(originalDbEntity, imported));
+    }
+
+    /**
+     * Compare same attributes in model and db
+     */
+    @Override
+    Collection<MergerToken> createTokensForSame(MergerDiffPair<DbAttribute> same) {
+        List<MergerToken> tokens = new LinkedList<>();
+
+        // isMandatory flag
+        checkMandatory(same.getOriginal(), same.getImported(), tokens);
+        // check type (including max length, scale and precision)
+        checkType(same.getOriginal(), same.getImported(), tokens);
+
+        // not implemented yet
+        // isGenerated flag
+        //checkIsGenerated(same.getOriginal(), same.getImported(), tokens);
+
+
+        return tokens;
+    }
+
+    private void checkMandatory(DbAttribute original, DbAttribute imported, List<MergerToken> tokens) {
+        if(original.isMandatory() == imported.isMandatory()) {
+            return;
+        }
+
+        DbEntity originalDbEntity = original.getEntity();
+        if (original.isMandatory()) {
+            if (valueForNull.hasValueFor(originalDbEntity, original)) {
+                tokens.add(getTokenFactory().createSetValueForNullToDb(originalDbEntity, original, valueForNull));
+            }
+            tokens.add(getTokenFactory().createSetNotNullToDb(originalDbEntity, original));
+        } else {
+            tokens.add(getTokenFactory().createSetAllowNullToDb(originalDbEntity, original));
+        }
+    }
+
+    /**
+     * Check whether attributes have same type, max length, scale and precision
+     * @param original attribute in model
+     * @param imported attribute from db
+     * @return true if attributes not same
+     */
+    private boolean needUpdate(DbAttribute original, DbAttribute imported) {
+        if(original.getType() != imported.getType()) {
+            return true;
+        }
+
+        if(original.getMaxLength() != imported.getMaxLength()) {
+            return true;
+        }
+
+        if(original.getScale() != imported.getScale()) {
+            return true;
+        }
+
+        if(original.getAttributePrecision() != imported.getAttributePrecision()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    private void checkType(DbAttribute original, DbAttribute imported, List<MergerToken> tokens) {
+        if(!needUpdate(original, imported)) {
+            return;
+        }
+        DbEntity originalDbEntity = original.getEntity();
+        tokens.add(getTokenFactory().createSetColumnTypeToDb(originalDbEntity, original, imported));
+    }
+
+    private void checkIsGenerated(DbAttribute original, DbAttribute imported, List<MergerToken> tokens) {
+        if(original.isGenerated() == imported.isGenerated()) {
+            return;
+        }
+
+
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/46c8ded5/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DbEntityDictionary.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DbEntityDictionary.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DbEntityDictionary.java
new file mode 100644
index 0000000..9114711
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DbEntityDictionary.java
@@ -0,0 +1,44 @@
+/*****************************************************************
+ *   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.cayenne.dbsync.merge;
+
+import java.util.Collection;
+
+import org.apache.cayenne.map.DataMap;
+import org.apache.cayenne.map.DbEntity;
+
+class DbEntityDictionary extends MergerDictionary<DbEntity> {
+
+    private final DataMap container;
+
+    DbEntityDictionary(DataMap container) {
+        this.container = container;
+    }
+
+    @Override
+    String getName(DbEntity entity) {
+        return entity.getFullyQualifiedName();
+    }
+
+    @Override
+    Collection<DbEntity> getAll() {
+        return container.getDbEntities();
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/46c8ded5/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DbEntityMerger.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DbEntityMerger.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DbEntityMerger.java
new file mode 100644
index 0000000..b718005
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DbEntityMerger.java
@@ -0,0 +1,108 @@
+/*****************************************************************
+ *   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.cayenne.dbsync.merge;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.apache.cayenne.dbsync.merge.factory.MergerTokenFactory;
+import org.apache.cayenne.dbsync.merge.token.MergerToken;
+import org.apache.cayenne.map.Attribute;
+import org.apache.cayenne.map.DataMap;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.DetectedDbEntity;
+
+class DbEntityMerger extends AbstractMerger<DataMap, DbEntity> {
+
+    private final boolean skipPKTokens;
+
+    DbEntityMerger(MergerTokenFactory tokenFactory, boolean skipPKTokens, DataMap original, DataMap imported) {
+        super(tokenFactory, original, imported);
+        this.skipPKTokens = skipPKTokens;
+    }
+
+    @Override
+    MergerDictionaryDiff<DbEntity> createDiff(DataMap original, DataMap imported) {
+        return new MergerDictionaryDiff.Builder<DbEntity>()
+                .originalDictionary(new DbEntityDictionary(original))
+                .importedDictionary(new DbEntityDictionary(imported))
+                .build();
+    }
+
+    /**
+     * Generate Drop Table in DB token
+     * @param imported DbEntity not found in model but found in DB
+     */
+    @Override
+    Collection<MergerToken> createTokensForMissingOriginal(DbEntity imported) {
+        return Collections.singleton(getTokenFactory().createDropTableToDb(imported));
+    }
+
+    /**
+     * Generate Create Table in model token
+     * @param original DbEntity found in model but not found in DB
+     */
+    @Override
+    Collection<MergerToken> createTokensForMissingImported(DbEntity original) {
+        return Collections.singleton(getTokenFactory().createCreateTableToDb(original));
+    }
+
+    /**
+     * Compare same entities. For now we check primary keys.
+     * @param same found DbEntities in model and db
+     */
+    @Override
+    Collection<MergerToken> createTokensForSame(MergerDiffPair<DbEntity> same) {
+        if(skipPKTokens) {
+            return null;
+        }
+        return checkPrimaryKeyChange(same.getOriginal(), same.getImported());
+    }
+
+    private Collection<MergerToken> checkPrimaryKeyChange(DbEntity original, DbEntity imported) {
+        Collection<DbAttribute> primaryKeyOriginal = imported.getPrimaryKeys();
+        Collection<DbAttribute> primaryKeyNew = original.getPrimaryKeys();
+
+        String primaryKeyName = null;
+        if (imported instanceof DetectedDbEntity) {
+            primaryKeyName = ((DetectedDbEntity) imported).getPrimaryKeyName();
+        }
+
+        if (upperCaseEntityNames(primaryKeyOriginal).equals(upperCaseEntityNames(primaryKeyNew))) {
+            return null;
+        }
+        return Collections.singleton(
+                getTokenFactory().createSetPrimaryKeyToDb(
+                        original, primaryKeyOriginal, primaryKeyNew, primaryKeyName
+                )
+        );
+    }
+
+    private Set<String> upperCaseEntityNames(Collection<? extends Attribute> attributes) {
+        Set<String> names = new HashSet<String>();
+        for (Attribute attr : attributes) {
+            names.add(attr.getName().toUpperCase());
+        }
+        return names;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/46c8ded5/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DbMerger.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DbMerger.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DbMerger.java
index 7adb0e5..9da71b5 100644
--- a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DbMerger.java
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DbMerger.java
@@ -19,6 +19,11 @@
 package org.apache.cayenne.dbsync.merge;
 
 import org.apache.cayenne.dbsync.merge.factory.MergerTokenFactory;
+import org.apache.cayenne.dbsync.merge.token.AddRelationshipToDb;
+import org.apache.cayenne.dbsync.merge.token.EmptyValueForNullProvider;
+import org.apache.cayenne.dbsync.merge.token.MergerToken;
+import org.apache.cayenne.dbsync.merge.token.TokenComparator;
+import org.apache.cayenne.dbsync.merge.token.ValueForNullProvider;
 import org.apache.cayenne.dbsync.reverse.filters.FiltersConfig;
 import org.apache.cayenne.dbsync.reverse.filters.PatternFilter;
 import org.apache.cayenne.dbsync.reverse.filters.TableFilter;
@@ -42,6 +47,7 @@ import java.util.Set;
 /**
  * Wraps an algorithm to traverse a {@link DataMap} and create a group of {@link MergerToken}s that can be used to
  * synchronize data store and Cayenne model.
+ * @deprecated
  */
 public class DbMerger {
 
@@ -85,13 +91,12 @@ public class DbMerger {
 
     private static boolean havePair(Collection<DbJoin> j2s, DbJoin j1) {
         for (DbJoin j2 : j2s) {
-            if (!isNull(j1.getSource()) && !isNull(j1.getTarget()) && !isNull(j2.getSource())
-                    && !isNull(j2.getTarget())
+            if (       !isNull(j1.getSource()) && !isNull(j1.getTarget())
+                    && !isNull(j2.getSource()) && !isNull(j2.getTarget())
                     && j1.getSource().getEntity().getName().equalsIgnoreCase(j2.getSource().getEntity().getName())
                     && j1.getTarget().getEntity().getName().equalsIgnoreCase(j2.getTarget().getEntity().getName())
                     && j1.getSourceName().equalsIgnoreCase(j2.getSourceName())
                     && j1.getTargetName().equalsIgnoreCase(j2.getTargetName())) {
-
                 return true;
             }
         }
@@ -351,6 +356,7 @@ public class DbMerger {
         return null;
     }
 
+    @Deprecated
     public static class Builder {
         private DbMerger merger;
 

http://git-wip-us.apache.org/repos/asf/cayenne/blob/46c8ded5/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DbRelationshipDictionary.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DbRelationshipDictionary.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DbRelationshipDictionary.java
new file mode 100644
index 0000000..4d5d74c
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DbRelationshipDictionary.java
@@ -0,0 +1,83 @@
+/*****************************************************************
+ *   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.cayenne.dbsync.merge;
+
+import java.util.Collection;
+import java.util.TreeSet;
+
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.DbJoin;
+import org.apache.cayenne.map.DbRelationship;
+
+class DbRelationshipDictionary extends MergerDictionary<DbRelationship> {
+
+    private final DbEntity container;
+
+    DbRelationshipDictionary(DbEntity container) {
+        this.container = container;
+    }
+
+    @Override
+    String getName(DbRelationship entity) {
+        return new Signature(entity).getName();
+    }
+
+    @Override
+    Collection<DbRelationship> getAll() {
+        return container.getRelationships();
+    }
+
+    /**
+     * Signature of DbRelationship is sorted strings generated from its DbJoins
+     */
+    private static class Signature {
+        private final DbRelationship relationship;
+
+        private String[] joinSignature;
+
+        private Signature(DbRelationship relationship) {
+            this.relationship = relationship;
+            build();
+        }
+
+        public String getName() {
+            if(joinSignature.length == 0) {
+                return "";
+            }
+            String name = joinSignature[0];
+            for(int i=1; i<joinSignature.length; i++) {
+                name += "|" + joinSignature[i];
+            }
+            return name;
+        }
+
+        private void build() {
+            TreeSet<String> joins = new TreeSet<>();
+            for(DbJoin join : relationship.getJoins()) {
+                joins.add(
+                        (join.getSource() == null ? "~" : join.getSource().getName()) + "." + join.getSourceName()
+                        + ">" +
+                        (join.getTarget() == null ? "~" : join.getTarget().getName()) + "." + join.getTargetName()
+                );
+            }
+            joinSignature = joins.toArray(new String[0]);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/46c8ded5/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DbRelationshipMerger.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DbRelationshipMerger.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DbRelationshipMerger.java
new file mode 100644
index 0000000..a094cb2
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DbRelationshipMerger.java
@@ -0,0 +1,127 @@
+/*****************************************************************
+ *   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.cayenne.dbsync.merge;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import org.apache.cayenne.dbsync.merge.factory.MergerTokenFactory;
+import org.apache.cayenne.dbsync.merge.token.MergerToken;
+import org.apache.cayenne.map.DataMap;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.DbJoin;
+import org.apache.cayenne.map.DbRelationship;
+
+/**
+ * Merger of relationships.
+ * Scoped to single DbEntity and it's counterpart.
+ */
+public class DbRelationshipMerger extends AbstractMerger<DbEntity, DbRelationship> {
+
+    private final boolean skipRelationshipsTokens;
+
+    DbRelationshipMerger(MergerTokenFactory tokenFactory,
+                         DataMap original, DataMap imported, boolean skipRelationshipsTokens) {
+        super(tokenFactory, original, imported);
+        this.skipRelationshipsTokens = skipRelationshipsTokens;
+    }
+
+    @Override
+    MergerDictionaryDiff<DbRelationship> createDiff(DbEntity original, DbEntity imported) {
+        return new MergerDictionaryDiff.Builder<DbRelationship>()
+                .originalDictionary(new DbRelationshipDictionary(original))
+                .importedDictionary(new DbRelationshipDictionary(imported))
+                .build();
+    }
+
+    private DbEntity getOriginalDbEntity(DbRelationship relationship) {
+        return originalDataMap.getDbEntity(relationship.getSourceEntity().getName());
+    }
+
+    /**
+     * @param imported DbRelationship that is in db but not in model
+     * @return generated tokens
+     */
+    @Override
+    Collection<MergerToken> createTokensForMissingOriginal(DbRelationship imported) {
+        DbEntity originalDbEntity = getOriginalDbEntity(imported);
+        DbEntity targetEntity = originalDataMap.getDbEntity(imported.getTargetEntityName());
+
+        if (targetEntity == null) {
+            return null;
+        }
+
+        imported.setSourceEntity(originalDbEntity);
+        imported.setTargetEntityName(targetEntity);
+
+        // manipulate the joins to match the DbAttributes in the model
+        for (DbJoin join : imported.getJoins()) {
+            DbAttribute sourceAttr = findDbAttribute(originalDbEntity, join.getSourceName());
+            if (sourceAttr != null) {
+                join.setSourceName(sourceAttr.getName());
+            }
+            DbAttribute targetAttr = findDbAttribute(targetEntity, join.getTargetName());
+            if (targetAttr != null) {
+                join.setTargetName(targetAttr.getName());
+            }
+        }
+        // Add all relationships. Tokens will decide whether or not to execute
+        MergerToken token = getTokenFactory().createDropRelationshipToDb(originalDbEntity, imported);
+        return Collections.singleton(token);
+    }
+
+    /**
+     * @param original DbRelationship that is in model but not in db
+     * @return generated tokens
+     */
+    @Override
+    Collection<MergerToken> createTokensForMissingImported(DbRelationship original) {
+        if(skipRelationshipsTokens) {
+            return null;
+        }
+        DbEntity originalDbEntity = getOriginalDbEntity(original);
+        MergerToken token = getTokenFactory().createAddRelationshipToDb(originalDbEntity, original);
+        return Collections.singleton(token);
+    }
+
+    /**
+     *
+     * @param same pair of found in model and in db DbRelationships
+     * @return generated tokens
+     */
+    @Override
+    Collection<MergerToken> createTokensForSame(MergerDiffPair<DbRelationship> same) {
+        return null;
+    }
+
+    /**
+     * case insensitive search for a {@link DbAttribute} in a {@link DbEntity}
+     * by name
+     */
+    private DbAttribute findDbAttribute(DbEntity entity, String caseInsensitiveName) {
+        for (DbAttribute a : entity.getAttributes()) {
+            if (a.getName().equalsIgnoreCase(caseInsensitiveName)) {
+                return a;
+            }
+        }
+        return null;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/46c8ded5/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DefaultModelMergeDelegate.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DefaultModelMergeDelegate.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DefaultModelMergeDelegate.java
deleted file mode 100644
index 816729d..0000000
--- a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DefaultModelMergeDelegate.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * 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.cayenne.dbsync.merge;
-
-import org.apache.cayenne.map.DbAttribute;
-import org.apache.cayenne.map.DbEntity;
-import org.apache.cayenne.map.DbRelationship;
-import org.apache.cayenne.map.ObjAttribute;
-import org.apache.cayenne.map.ObjEntity;
-import org.apache.cayenne.map.ObjRelationship;
-
-/**
- * A default noop implementation of {@link ModelMergeDelegate}.
- */
-public class DefaultModelMergeDelegate implements ModelMergeDelegate {
-
-    @Override
-    public void dbAttributeAdded(DbAttribute att) {
-    }
-
-    @Override
-    public void dbAttributeModified(DbAttribute att) {
-    }
-
-    @Override
-    public void dbAttributeRemoved(DbAttribute att) {
-    }
-
-    @Override
-    public void dbEntityAdded(DbEntity ent) {
-    }
-
-    @Override
-    public void dbEntityRemoved(DbEntity ent) {
-    }
-
-    @Override
-    public void dbRelationshipAdded(DbRelationship rel) {
-    }
-
-    @Override
-    public void dbRelationshipRemoved(DbRelationship rel) {
-    }
-
-    @Override
-    public void objAttributeAdded(ObjAttribute att) {
-    }
-
-    @Override
-    public void objAttributeModified(ObjAttribute att) {
-    }
-
-    @Override
-    public void objAttributeRemoved(ObjAttribute att) {
-    }
-
-    @Override
-    public void objEntityAdded(ObjEntity ent) {
-    }
-
-    @Override
-    public void objEntityRemoved(ObjEntity ent) {
-    }
-
-    @Override
-    public void objRelationshipAdded(ObjRelationship rel) {
-    }
-
-    @Override
-    public void objRelationshipRemoved(ObjRelationship rel) {
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/46c8ded5/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DefaultValueForNullProvider.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DefaultValueForNullProvider.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DefaultValueForNullProvider.java
deleted file mode 100644
index 9d4e0cd..0000000
--- a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DefaultValueForNullProvider.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*****************************************************************
- *   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.cayenne.dbsync.merge;
-
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import org.apache.cayenne.access.jdbc.SQLParameterBinding;
-import org.apache.cayenne.map.DbAttribute;
-import org.apache.cayenne.map.DbEntity;
-
-public class DefaultValueForNullProvider implements ValueForNullProvider {
-
-    private Map<String, SQLParameterBinding> values = new HashMap<>();
-
-    public void set(DbEntity entity, DbAttribute column, Object value, int type) {
-        values.put(createKey(entity, column), new SQLParameterBinding(value, type, column
-                .getAttributePrecision()));
-    }
-
-    protected SQLParameterBinding get(DbEntity entity, DbAttribute column) {
-        return values.get(createKey(entity, column));
-    }
-
-    public List<String> createSql(DbEntity entity, DbAttribute column) {
-        SQLParameterBinding value = get(entity, column);
-        if (value == null) {
-            return Collections.emptyList();
-        }
-
-        // TODO: change things so it is possible to use prepared statements here
-        return Collections.singletonList("UPDATE " + entity.getFullyQualifiedName()
-                + " SET " + column.getName() + "='" + value.getValue() + "' WHERE " + column.getName() + " IS NULL");
-    }
-
-    public boolean hasValueFor(DbEntity entity, DbAttribute column) {
-        return values.containsKey(createKey(entity, column));
-    }
-
-    private String createKey(DbEntity entity, DbAttribute attribute) {
-        return (entity.getFullyQualifiedName() + "." + attribute.getName()).toUpperCase();
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/46c8ded5/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DropColumnToDb.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DropColumnToDb.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DropColumnToDb.java
deleted file mode 100644
index dec3297..0000000
--- a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DropColumnToDb.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*****************************************************************
- *   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.cayenne.dbsync.merge;
-
-import java.util.Collections;
-import java.util.List;
-
-import org.apache.cayenne.dba.DbAdapter;
-import org.apache.cayenne.dba.QuotingStrategy;
-import org.apache.cayenne.dbsync.merge.factory.MergerTokenFactory;
-import org.apache.cayenne.map.DbAttribute;
-import org.apache.cayenne.map.DbEntity;
-
-public class DropColumnToDb extends AbstractToDbToken.EntityAndColumn {
-
-    public DropColumnToDb(DbEntity entity, DbAttribute column) {
-        super("Drop Column", entity, column);
-    }
-
-    @Override
-    public List<String> createSql(DbAdapter adapter) {
-        StringBuilder sqlBuffer = new StringBuilder();
-        QuotingStrategy context = adapter.getQuotingStrategy();
-        sqlBuffer.append("ALTER TABLE ");
-        sqlBuffer.append(context.quotedFullyQualifiedName(getEntity()));
-        sqlBuffer.append(" DROP COLUMN ");
-        sqlBuffer.append(context.quotedName(getColumn()));
-
-        return Collections.singletonList(sqlBuffer.toString());
-    }
-
-    public MergerToken createReverse(MergerTokenFactory factory) {
-        return factory.createAddColumnToModel(getEntity(), getColumn());
-    }
-
-    @Override
-    public int compareTo(MergerToken o) {
-        // add all AddRelationshipToDb to the end.
-        if (o instanceof DropRelationshipToDb) {
-            return 1;
-        }
-        return super.compareTo(o);
-    }
-
-}