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:39 UTC

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

http://git-wip-us.apache.org/repos/asf/cayenne/blob/46c8ded5/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/SetPrimaryKeyToDb.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/SetPrimaryKeyToDb.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/SetPrimaryKeyToDb.java
deleted file mode 100644
index 077b6ef..0000000
--- a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/SetPrimaryKeyToDb.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.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;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Iterator;
-import java.util.List;
-
-public class SetPrimaryKeyToDb extends AbstractToDbToken.Entity {
-
-    private Collection<DbAttribute> primaryKeyOriginal;
-    private Collection<DbAttribute> primaryKeyNew;
-    private String detectedPrimaryKeyName;
-
-    public SetPrimaryKeyToDb(DbEntity entity, Collection<DbAttribute> primaryKeyOriginal,
-            Collection<DbAttribute> primaryKeyNew, String detectedPrimaryKeyName) {
-        super("Set Primary Key", entity);
-
-        this.primaryKeyOriginal = primaryKeyOriginal;
-        this.primaryKeyNew = primaryKeyNew;
-        this.detectedPrimaryKeyName = detectedPrimaryKeyName;
-    }
-
-    @Override
-    public List<String> createSql(DbAdapter adapter) {
-        List<String> sqls = new ArrayList<String>();
-        if (!primaryKeyOriginal.isEmpty()) {
-            appendDropOriginalPrimaryKeySQL(adapter, sqls);
-        }
-        appendAddNewPrimaryKeySQL(adapter, sqls);
-        return sqls;
-    }
-
-    protected void appendDropOriginalPrimaryKeySQL(DbAdapter adapter, List<String> sqls) {
-        if (detectedPrimaryKeyName == null) {
-            return;
-        }
-        sqls.add("ALTER TABLE " + adapter.getQuotingStrategy().quotedFullyQualifiedName(getEntity())
-                + " DROP CONSTRAINT " + detectedPrimaryKeyName);
-    }
-
-    protected void appendAddNewPrimaryKeySQL(DbAdapter adapter, List<String> sqls) {
-        QuotingStrategy quotingStrategy = adapter.getQuotingStrategy();
-
-        StringBuilder sql = new StringBuilder();
-        sql.append("ALTER TABLE ");
-        sql.append(quotingStrategy.quotedFullyQualifiedName(getEntity()));
-        sql.append(" ADD PRIMARY KEY (");
-        for (Iterator<DbAttribute> it = primaryKeyNew.iterator(); it.hasNext();) {
-            sql.append(quotingStrategy.quotedName(it.next()));
-            if (it.hasNext()) {
-                sql.append(", ");
-            }
-        }
-        sql.append(")");
-        sqls.add(sql.toString());
-    }
-
-    @Override
-    public MergerToken createReverse(MergerTokenFactory factory) {
-        return factory.createSetPrimaryKeyToModel(getEntity(), primaryKeyNew, primaryKeyOriginal,
-                detectedPrimaryKeyName);
-    }
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/46c8ded5/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/SetPrimaryKeyToModel.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/SetPrimaryKeyToModel.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/SetPrimaryKeyToModel.java
deleted file mode 100644
index a2198ba..0000000
--- a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/SetPrimaryKeyToModel.java
+++ /dev/null
@@ -1,80 +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.event.AttributeEvent;
-
-import java.util.Collection;
-import java.util.HashSet;
-import java.util.Set;
-
-public class SetPrimaryKeyToModel extends AbstractToModelToken.Entity {
-
-    private Collection<DbAttribute> primaryKeyOriginal;
-    private Collection<DbAttribute> primaryKeyNew;
-    private String detectedPrimaryKeyName;
-    private Set<String> primaryKeyNewAttributeNames = new HashSet<String>();
-
-    public SetPrimaryKeyToModel(DbEntity entity,
-            Collection<DbAttribute> primaryKeyOriginal,
-            Collection<DbAttribute> primaryKeyNew, String detectedPrimaryKeyName) {
-        super("Set Primary Key", entity);
-        
-        this.primaryKeyOriginal = primaryKeyOriginal;
-        this.primaryKeyNew = primaryKeyNew;
-        this.detectedPrimaryKeyName = detectedPrimaryKeyName;
-        
-        for (DbAttribute attr : primaryKeyNew) {
-            primaryKeyNewAttributeNames.add(attr.getName().toUpperCase());
-        }
-    }
-
-    @Override
-    public MergerToken createReverse(MergerTokenFactory factory) {
-        return factory.createSetPrimaryKeyToDb(
-                getEntity(),
-                primaryKeyNew,
-                primaryKeyOriginal,
-                detectedPrimaryKeyName);
-    }
-
-    @Override
-    public void execute(MergerContext mergerContext) {
-        DbEntity e = getEntity();
-
-        for (DbAttribute attr : e.getAttributes()) {
-
-            boolean wasPrimaryKey = attr.isPrimaryKey();
-            boolean willBePrimaryKey = primaryKeyNewAttributeNames.contains(attr
-                    .getName()
-                    .toUpperCase());
-
-            if (wasPrimaryKey != willBePrimaryKey) {
-                attr.setPrimaryKey(willBePrimaryKey);
-                e.dbAttributeChanged(new AttributeEvent(this, attr, e));
-                mergerContext.getDelegate().dbAttributeModified(attr);
-            }
-
-        }
-
-    }
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/46c8ded5/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/SetValueForNullToDb.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/SetValueForNullToDb.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/SetValueForNullToDb.java
deleted file mode 100644
index 340f2bf..0000000
--- a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/SetValueForNullToDb.java
+++ /dev/null
@@ -1,48 +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.dbsync.merge.factory.MergerTokenFactory;
-import org.apache.cayenne.map.DbAttribute;
-import org.apache.cayenne.map.DbEntity;
-
-import java.util.List;
-
-
-public class SetValueForNullToDb extends AbstractToDbToken.EntityAndColumn {
-    
-    private ValueForNullProvider valueForNullProvider;
-
-    public SetValueForNullToDb(DbEntity entity, DbAttribute column, ValueForNullProvider valueForNullProvider) {
-        super("Set value for null", entity, column);
-        this.valueForNullProvider = valueForNullProvider;
-    }
-    
-    @Override
-    public List<String> createSql(DbAdapter adapter) {
-        return valueForNullProvider.createSql(getEntity(), getColumn());
-    }
-
-    @Override
-    public MergerToken createReverse(MergerTokenFactory factory) {
-        return new DummyReverseToken(this);
-    }
-    
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/46c8ded5/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/TokenComparator.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/TokenComparator.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/TokenComparator.java
deleted file mode 100644
index 4cc312f..0000000
--- a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/TokenComparator.java
+++ /dev/null
@@ -1,46 +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.Comparator;
-
-/**
- * Simple sort of merge tokens.
- * Just move all relationships creation tokens to the end of the list.
- */
-public class TokenComparator implements Comparator<MergerToken> {
-
-    @Override
-    public int compare(MergerToken o1, MergerToken o2) {
-        if (o1 instanceof AbstractToDbToken && o2 instanceof AbstractToDbToken) {
-            return ((AbstractToDbToken) o1).compareTo(o2);
-        }
-
-        if (o1 instanceof AddRelationshipToModel && o2 instanceof AddRelationshipToModel) {
-            return 0;
-        }
-
-        if (!(o1 instanceof AddRelationshipToModel || o2 instanceof AddRelationshipToModel)) {
-            return o1.getClass().getSimpleName().compareTo(o2.getClass().getSimpleName());
-        }
-
-        return o1 instanceof AddRelationshipToModel ? 1 : -1;
-    }
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/46c8ded5/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/ValueForNullProvider.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/ValueForNullProvider.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/ValueForNullProvider.java
deleted file mode 100644
index 6bbd68a..0000000
--- a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/ValueForNullProvider.java
+++ /dev/null
@@ -1,41 +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 java.util.List;
-
-/**
- * Class that will be used to set value for null on not null columns
- */
-public interface ValueForNullProvider {
-
-    /**
-     * @return true if there exist a value that should be inserted for null values
-     */
-    boolean hasValueFor(DbEntity entity, DbAttribute column);
-
-    /**
-     * @return a {@link List} of sql to set value for null
-     */
-    List<String> createSql(DbEntity entity, DbAttribute column);
-
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/46c8ded5/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/context/EntityMergeSupport.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/context/EntityMergeSupport.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/context/EntityMergeSupport.java
new file mode 100644
index 0000000..9bb2a84
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/context/EntityMergeSupport.java
@@ -0,0 +1,516 @@
+/*****************************************************************
+ *   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.context;
+
+import org.apache.cayenne.dba.TypesMapping;
+import org.apache.cayenne.dbsync.filter.NameFilter;
+import org.apache.cayenne.dbsync.naming.NameBuilder;
+import org.apache.cayenne.dbsync.naming.ObjectNameGenerator;
+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;
+import org.apache.cayenne.map.Entity;
+import org.apache.cayenne.map.ObjAttribute;
+import org.apache.cayenne.map.ObjEntity;
+import org.apache.cayenne.map.ObjRelationship;
+import org.apache.cayenne.map.Relationship;
+import org.apache.cayenne.util.DeleteRuleUpdater;
+import org.apache.cayenne.util.EntityMergeListener;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Implements methods for entity merging.
+ */
+public class EntityMergeSupport {
+
+    private static final Log LOGGER = LogFactory.getLog(EntityMergeSupport.class);
+
+    private static final Map<String, String> CLASS_TO_PRIMITIVE;
+
+    static {
+        CLASS_TO_PRIMITIVE = new HashMap<>();
+        CLASS_TO_PRIMITIVE.put(Byte.class.getName(), "byte");
+        CLASS_TO_PRIMITIVE.put(Long.class.getName(), "long");
+        CLASS_TO_PRIMITIVE.put(Double.class.getName(), "double");
+        CLASS_TO_PRIMITIVE.put(Boolean.class.getName(), "boolean");
+        CLASS_TO_PRIMITIVE.put(Float.class.getName(), "float");
+        CLASS_TO_PRIMITIVE.put(Short.class.getName(), "short");
+        CLASS_TO_PRIMITIVE.put(Integer.class.getName(), "int");
+    }
+
+    private final ObjectNameGenerator nameGenerator;
+    private final List<EntityMergeListener> listeners;
+    private final boolean removingMeaningfulFKs;
+    private final NameFilter meaningfulPKsFilter;
+    private final boolean usingPrimitives;
+
+    public EntityMergeSupport(ObjectNameGenerator nameGenerator,
+                              NameFilter meaningfulPKsFilter,
+                              boolean removingMeaningfulFKs,
+                              boolean usingPrimitives) {
+
+        this.listeners = new ArrayList<>();
+        this.nameGenerator = nameGenerator;
+        this.removingMeaningfulFKs = removingMeaningfulFKs;
+        this.meaningfulPKsFilter = meaningfulPKsFilter;
+        this.usingPrimitives = usingPrimitives;
+
+        // will ensure that all created ObjRelationships would have
+        // default delete rule
+        addEntityMergeListener(DeleteRuleUpdater.getEntityMergeListener());
+    }
+
+    public boolean isRemovingMeaningfulFKs() {
+        return removingMeaningfulFKs;
+    }
+
+
+    /**
+     * Updates each one of the collection of ObjEntities, adding attributes and
+     * relationships based on the current state of its DbEntity.
+     *
+     * @return true if any ObjEntity has changed as a result of synchronization.
+     */
+    public boolean synchronizeWithDbEntities(Iterable<ObjEntity> objEntities) {
+        boolean changed = false;
+        for (ObjEntity nextEntity : objEntities) {
+            if (synchronizeWithDbEntity(nextEntity)) {
+                changed = true;
+            }
+        }
+
+        return changed;
+    }
+
+    /**
+     * Updates ObjEntity attributes and relationships based on the current state
+     * of its DbEntity.
+     *
+     * @return true if the ObjEntity has changed as a result of synchronization.
+     */
+    public boolean synchronizeWithDbEntity(ObjEntity entity) {
+
+        if (entity == null) {
+            return false;
+        }
+
+        DbEntity dbEntity = entity.getDbEntity();
+        if (dbEntity == null) {
+            return false;
+        }
+
+        boolean changed = false;
+
+        if (removingMeaningfulFKs) {
+            changed = getRidOfAttributesThatAreNowSrcAttributesForRelationships(entity);
+        }
+
+        changed |= addMissingAttributes(entity);
+        changed |= addMissingRelationships(entity);
+
+        return changed;
+    }
+
+    /**
+     * @since 4.0
+     */
+    public boolean synchronizeOnDbAttributeAdded(ObjEntity entity, DbAttribute dbAttribute) {
+
+        Collection<DbRelationship> incomingRels = getIncomingRelationships(dbAttribute.getEntity());
+        if (shouldAddToObjEntity(entity, dbAttribute, incomingRels)) {
+            addMissingAttribute(entity, dbAttribute);
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * @since 4.0
+     */
+    public boolean synchronizeOnDbRelationshipAdded(ObjEntity entity, DbRelationship dbRelationship) {
+
+        if (shouldAddToObjEntity(entity, dbRelationship)) {
+            addMissingRelationship(entity, dbRelationship);
+        }
+
+        return true;
+    }
+
+    private boolean addMissingRelationships(ObjEntity entity) {
+        List<DbRelationship> relationshipsToAdd = getRelationshipsToAdd(entity);
+        if (relationshipsToAdd.isEmpty()) {
+            return false;
+        }
+
+        for (DbRelationship dr : relationshipsToAdd) {
+            addMissingRelationship(entity, dr);
+        }
+
+        return true;
+    }
+
+    private boolean createObjRelationship(ObjEntity entity, DbRelationship dr, String targetEntityName) {
+        ObjRelationship or = new ObjRelationship();
+        or.setName(NameBuilder.builder(or, entity)
+                .baseName(nameGenerator.relationshipName(dr))
+                .name());
+
+        or.addDbRelationship(dr);
+        Map<String, ObjEntity> objEntities = entity.getDataMap().getSubclassesForObjEntity(entity);
+
+        boolean hasFlattingAttributes = false;
+        boolean needGeneratedEntity = true;
+
+        if (objEntities.containsKey(targetEntityName)) {
+            needGeneratedEntity = false;
+        }
+
+        for (ObjEntity subObjEntity : objEntities.values()) {
+            for (ObjAttribute objAttribute : subObjEntity.getAttributes()) {
+                String path = objAttribute.getDbAttributePath();
+                if (path != null) {
+                    if (path.startsWith(or.getDbRelationshipPath())) {
+                        hasFlattingAttributes = true;
+                        break;
+                    }
+                }
+            }
+        }
+
+        if (!hasFlattingAttributes) {
+            if (needGeneratedEntity) {
+                or.setTargetEntityName(targetEntityName);
+                or.setSourceEntity(entity);
+            }
+
+            entity.addRelationship(or);
+            fireRelationshipAdded(or);
+        }
+
+        return needGeneratedEntity;
+    }
+
+    private boolean addMissingAttributes(ObjEntity entity) {
+        boolean changed = false;
+
+        for (DbAttribute da : getAttributesToAdd(entity)) {
+            addMissingAttribute(entity, da);
+            changed = true;
+        }
+        return changed;
+    }
+
+    private void addMissingRelationship(ObjEntity entity, DbRelationship dbRelationship) {
+
+        // getting DataMap from DbRelationship's source entity. This is the only object in our arguments that
+        // is guaranteed to be a part of the map....
+        DataMap dataMap = dbRelationship.getSourceEntity().getDataMap();
+
+        DbEntity targetEntity = dbRelationship.getTargetEntity();
+        Collection<ObjEntity> mappedObjEntities = dataMap.getMappedEntities(targetEntity);
+        if (mappedObjEntities.isEmpty()) {
+            if (targetEntity == null) {
+                targetEntity = new DbEntity(dbRelationship.getTargetEntityName());
+            }
+
+            if (dbRelationship.getTargetEntityName() != null) {
+                boolean needGeneratedEntity = createObjRelationship(entity, dbRelationship,
+                        nameGenerator.objEntityName(targetEntity));
+                if (needGeneratedEntity) {
+                    LOGGER.warn("Can't find ObjEntity for " + dbRelationship.getTargetEntityName());
+                    LOGGER.warn("Db Relationship (" + dbRelationship + ") will have GUESSED Obj Relationship reflection. ");
+                }
+            }
+        } else {
+            for (Entity mappedTarget : mappedObjEntities) {
+                createObjRelationship(entity, dbRelationship, mappedTarget.getName());
+            }
+        }
+    }
+
+    private void addMissingAttribute(ObjEntity entity, DbAttribute da) {
+        ObjAttribute oa = new ObjAttribute();
+        oa.setName(NameBuilder.builder(oa, entity)
+                .baseName(nameGenerator.objAttributeName(da))
+                .name());
+        oa.setEntity(entity);
+
+        String type = TypesMapping.getJavaBySqlType(da.getType());
+        if (usingPrimitives) {
+            String primitive = CLASS_TO_PRIMITIVE.get(type);
+            if (primitive != null) {
+                type = primitive;
+            }
+        }
+        oa.setType(type);
+        oa.setDbAttributePath(da.getName());
+        entity.addAttribute(oa);
+        fireAttributeAdded(oa);
+    }
+
+    private boolean getRidOfAttributesThatAreNowSrcAttributesForRelationships(ObjEntity entity) {
+        boolean changed = false;
+        for (DbAttribute da : getMeaningfulFKs(entity)) {
+            ObjAttribute oa = entity.getAttributeForDbAttribute(da);
+            while (oa != null) {
+                String attrName = oa.getName();
+                entity.removeAttribute(attrName);
+                changed = true;
+                oa = entity.getAttributeForDbAttribute(da);
+            }
+        }
+        return changed;
+    }
+
+    /**
+     * Returns a list of DbAttributes that are mapped to foreign keys.
+     *
+     * @since 1.2
+     */
+    public Collection<DbAttribute> getMeaningfulFKs(ObjEntity objEntity) {
+        List<DbAttribute> fks = new ArrayList<>(2);
+
+        for (ObjAttribute property : objEntity.getAttributes()) {
+            DbAttribute column = property.getDbAttribute();
+
+            // check if adding it makes sense at all
+            if (column != null && column.isForeignKey()) {
+                fks.add(column);
+            }
+        }
+
+        return fks;
+    }
+
+    /**
+     * Returns a list of attributes that exist in the DbEntity, but are missing
+     * from the ObjEntity.
+     */
+    protected List<DbAttribute> getAttributesToAdd(ObjEntity objEntity) {
+        DbEntity dbEntity = objEntity.getDbEntity();
+
+        List<DbAttribute> missing = new ArrayList<>();
+        Collection<DbRelationship> incomingRels = getIncomingRelationships(dbEntity);
+
+        for (DbAttribute dba : dbEntity.getAttributes()) {
+
+            if (shouldAddToObjEntity(objEntity, dba, incomingRels)) {
+                missing.add(dba);
+            }
+        }
+
+        return missing;
+    }
+
+    protected boolean shouldAddToObjEntity(ObjEntity entity, DbAttribute dbAttribute, Collection<DbRelationship> incomingRels) {
+
+        if (dbAttribute.getName() == null || entity.getAttributeForDbAttribute(dbAttribute) != null) {
+            return false;
+        }
+
+        boolean addMeaningfulPK = meaningfulPKsFilter.isIncluded(entity.getDbEntityName());
+
+        if (dbAttribute.isPrimaryKey() && !addMeaningfulPK) {
+            return false;
+        }
+
+        // check FK's
+        boolean isFK = false;
+        Iterator<DbRelationship> rit = dbAttribute.getEntity().getRelationships().iterator();
+        while (!isFK && rit.hasNext()) {
+            DbRelationship rel = rit.next();
+            for (DbJoin join : rel.getJoins()) {
+                if (join.getSource() == dbAttribute) {
+                    isFK = true;
+                    break;
+                }
+            }
+        }
+
+        if (addMeaningfulPK) {
+            if (!dbAttribute.isPrimaryKey() && isFK) {
+                return false;
+            }
+        } else {
+            if (isFK) {
+                return false;
+            }
+        }
+
+        // check incoming relationships
+        rit = incomingRels.iterator();
+        while (!isFK && rit.hasNext()) {
+            DbRelationship rel = rit.next();
+            for (DbJoin join : rel.getJoins()) {
+                if (join.getTarget() == dbAttribute) {
+                    isFK = true;
+                    break;
+                }
+            }
+        }
+
+        if (addMeaningfulPK) {
+            if (!dbAttribute.isPrimaryKey() && isFK) {
+                return false;
+            }
+        } else {
+            if (isFK) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    private boolean shouldAddToObjEntity(ObjEntity entity, DbRelationship dbRelationship) {
+        if(dbRelationship.getName() == null) {
+            return false;
+        }
+
+        for(Relationship relationship : entity.getRelationships()) {
+            ObjRelationship objRelationship = (ObjRelationship)relationship;
+            if(objRelationshipHasDbRelationship(objRelationship, dbRelationship)) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * @return true if objRelationship includes given dbRelationship
+     */
+    private boolean objRelationshipHasDbRelationship(ObjRelationship objRelationship, DbRelationship dbRelationship) {
+        for(DbRelationship relationship : objRelationship.getDbRelationships()) {
+
+            if(relationship.getSourceEntityName().equals(dbRelationship.getSourceEntityName())
+                    && relationship.getTargetEntityName().equals(dbRelationship.getTargetEntityName())
+                    && isSameAttributes(relationship.getSourceAttributes(), dbRelationship.getSourceAttributes())
+                    && isSameAttributes(relationship.getTargetAttributes(), dbRelationship.getTargetAttributes())) {
+                return true;
+            }
+
+        }
+        return false;
+    }
+
+
+    /**
+     * @param collection1 first collection to compare
+     * @param collection2 second collection to compare
+     * @return true if collections have same size and attributes in them have same names
+     */
+    private boolean isSameAttributes(Collection<DbAttribute> collection1, Collection<DbAttribute> collection2) {
+        if(collection1.size() != collection2.size()) {
+            return false;
+        }
+
+        if(collection1.isEmpty()) {
+            return true;
+        }
+
+        Iterator<DbAttribute> iterator1 = collection1.iterator();
+        Iterator<DbAttribute> iterator2 = collection2.iterator();
+        for(int i=0; i<collection1.size(); i++) {
+            DbAttribute attr1 = iterator1.next();
+            DbAttribute attr2 = iterator2.next();
+            if(!attr1.getName().equals(attr2.getName())) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    private Collection<DbRelationship> getIncomingRelationships(DbEntity entity) {
+        Collection<DbRelationship> incoming = new ArrayList<DbRelationship>();
+
+        for (DbEntity nextEntity : entity.getDataMap().getDbEntities()) {
+            for (DbRelationship relationship : nextEntity.getRelationships()) {
+
+                // TODO: PERFORMANCE 'getTargetEntity' is generally slow, called
+                // in this iterator it is showing (e.g. in YourKit profiles)..
+                // perhaps use cheaper 'getTargetEntityName()' or even better -
+                // pre-cache all relationships by target entity to avoid O(n)
+                // search ?
+                // (need to profile to prove the difference)
+                if (entity == relationship.getTargetEntity()) {
+                    incoming.add(relationship);
+                }
+            }
+        }
+
+        return incoming;
+    }
+
+    protected List<DbRelationship> getRelationshipsToAdd(ObjEntity objEntity) {
+        List<DbRelationship> missing = new ArrayList<DbRelationship>();
+        for (DbRelationship dbRel : objEntity.getDbEntity().getRelationships()) {
+            if (shouldAddToObjEntity(objEntity, dbRel)) {
+                missing.add(dbRel);
+            }
+        }
+
+        return missing;
+    }
+
+    /**
+     * Registers new EntityMergeListener
+     */
+    public void addEntityMergeListener(EntityMergeListener listener) {
+        listeners.add(listener);
+    }
+
+    /**
+     * Unregisters an EntityMergeListener
+     */
+    public void removeEntityMergeListener(EntityMergeListener listener) {
+        listeners.remove(listener);
+    }
+
+    /**
+     * Notifies all listeners that an ObjAttribute was added
+     */
+    protected void fireAttributeAdded(ObjAttribute attr) {
+        for (EntityMergeListener listener : listeners) {
+            listener.objAttributeAdded(attr);
+        }
+    }
+
+    /**
+     * Notifies all listeners that an ObjRelationship was added
+     */
+    protected void fireRelationshipAdded(ObjRelationship rel) {
+        for (EntityMergeListener listener : listeners) {
+            listener.objRelationshipAdded(rel);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/46c8ded5/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/context/MergeDirection.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/context/MergeDirection.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/context/MergeDirection.java
new file mode 100644
index 0000000..fe2e9ac
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/context/MergeDirection.java
@@ -0,0 +1,71 @@
+/*****************************************************************
+ *   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.context;
+
+/**
+ * Represent a merge direction that can be either from the model to the db or from the db to the model.
+ */
+public enum MergeDirection {
+
+    /**
+     * TO_DB Token means that changes was made in object model and should be reflected at DB
+     */
+    TO_DB("To DB"),
+
+    /**
+     * TO_MODEL Token represent database changes that should be allayed to object model
+     */
+    TO_MODEL("To Model");
+
+    private String name;
+
+    MergeDirection(String name) {
+        this.name = name;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public boolean isToDb() {
+        return (this == TO_DB);
+    }
+
+    public boolean isToModel() {
+        return (this == TO_MODEL);
+    }
+
+    @Override
+    public String toString() {
+        return getName();
+    }
+
+    public MergeDirection reverseDirection() {
+        switch (this) {
+            case TO_DB:
+                return TO_MODEL;
+            case TO_MODEL:
+                return TO_DB;
+            default:
+                throw new IllegalStateException("Invalid direction: " + this);
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/46c8ded5/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/context/MergerContext.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/context/MergerContext.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/context/MergerContext.java
new file mode 100644
index 0000000..b048952
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/context/MergerContext.java
@@ -0,0 +1,174 @@
+/*****************************************************************
+ *   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.context;
+
+import org.apache.cayenne.access.DataNode;
+import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.dbsync.filter.NameFilter;
+import org.apache.cayenne.dbsync.filter.NamePatternMatcher;
+import org.apache.cayenne.dbsync.merge.token.MergerToken;
+import org.apache.cayenne.dbsync.naming.DefaultObjectNameGenerator;
+import org.apache.cayenne.dbsync.naming.NoStemStemmer;
+import org.apache.cayenne.dbsync.naming.ObjectNameGenerator;
+import org.apache.cayenne.dbsync.reverse.dbload.DefaultModelMergeDelegate;
+import org.apache.cayenne.dbsync.reverse.dbload.ModelMergeDelegate;
+import org.apache.cayenne.map.DataMap;
+import org.apache.cayenne.validation.ValidationResult;
+
+import javax.sql.DataSource;
+import java.util.Objects;
+
+/**
+ * An object passed as an argument to {@link MergerToken#execute(MergerContext)}s that a
+ * {@link MergerToken} can do its work.
+ */
+public class MergerContext {
+
+    private DataMap dataMap;
+    private DataNode dataNode;
+    private ValidationResult validationResult;
+    private ModelMergeDelegate delegate;
+    private EntityMergeSupport entityMergeSupport;
+    private ObjectNameGenerator nameGenerator;
+
+    protected MergerContext() {
+    }
+
+    /**
+     * @since 4.0
+     */
+    public static Builder builder(DataMap dataMap) {
+        return new Builder(dataMap);
+    }
+
+    /**
+     * @since 4.0
+     */
+    public EntityMergeSupport getEntityMergeSupport() {
+        return entityMergeSupport;
+    }
+
+    /**
+     * Returns the DataMap that is the target of a the merge operation.
+     *
+     * @return the DataMap that is the target of a the merge operation.
+     */
+    public DataMap getDataMap() {
+        return dataMap;
+    }
+
+    public DataNode getDataNode() {
+        return dataNode;
+    }
+
+    public ValidationResult getValidationResult() {
+        return validationResult;
+    }
+
+    /**
+     * Returns a callback object that is invoked as the merge proceeds through tokens, modifying the DataMap.
+     *
+     * @return a callback object that is invoked as the merge proceeds through tokens, modifying the DataMap.
+     * @since 4.0
+     */
+    public ModelMergeDelegate getDelegate() {
+        return delegate;
+    }
+
+    /**
+     * @since 4.0
+     */
+    public ObjectNameGenerator getNameGenerator() {
+        return nameGenerator;
+    }
+
+    public static class Builder {
+
+        private MergerContext context;
+        private boolean usingPrimitives;
+        private NameFilter meaningfulPKsFilter;
+
+        private Builder(DataMap dataMap) {
+            this.context = new MergerContext();
+            this.context.dataMap = Objects.requireNonNull(dataMap);
+            this.context.validationResult = new ValidationResult();
+        }
+
+        public MergerContext build() {
+
+            // init missing defaults ...
+
+            if (context.delegate == null) {
+                delegate(new DefaultModelMergeDelegate());
+            }
+
+            if (context.dataNode == null) {
+                dataNode(new DataNode());
+            }
+
+            if(context.nameGenerator == null) {
+                context.nameGenerator = new DefaultObjectNameGenerator(NoStemStemmer.getInstance());
+            }
+
+            if(meaningfulPKsFilter == null) {
+                meaningfulPKsFilter = NamePatternMatcher.EXCLUDE_ALL;
+            }
+
+            context.entityMergeSupport = new EntityMergeSupport(context.nameGenerator,
+                    meaningfulPKsFilter,
+                    true,
+                    usingPrimitives);
+
+            return context;
+        }
+
+        public Builder delegate(ModelMergeDelegate delegate) {
+            context.delegate = Objects.requireNonNull(delegate);
+            return this;
+        }
+
+        public Builder nameGenerator(ObjectNameGenerator nameGenerator) {
+            this.context.nameGenerator = Objects.requireNonNull(nameGenerator);
+            return this;
+        }
+
+        public Builder usingPrimitives(boolean flag) {
+            this.usingPrimitives = flag;
+            return this;
+        }
+
+        public Builder dataNode(DataNode dataNode) {
+            this.context.dataNode = Objects.requireNonNull(dataNode);
+            return this;
+        }
+
+        public Builder meaningfulPKFilter(NameFilter filter) {
+            this.meaningfulPKsFilter = Objects.requireNonNull(filter);
+            return this;
+        }
+
+        public Builder syntheticDataNode(DataSource dataSource, DbAdapter adapter) {
+            DataNode dataNode = new DataNode();
+            dataNode.setDataSource(dataSource);
+            dataNode.setAdapter(adapter);
+            return dataNode(dataNode);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/46c8ded5/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/DB2MergerTokenFactory.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/DB2MergerTokenFactory.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/DB2MergerTokenFactory.java
index 586a5cd..5100184 100644
--- a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/DB2MergerTokenFactory.java
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/DB2MergerTokenFactory.java
@@ -19,8 +19,8 @@
 package org.apache.cayenne.dbsync.merge.factory;
 
 import org.apache.cayenne.dba.QuotingStrategy;
-import org.apache.cayenne.dbsync.merge.MergerToken;
-import org.apache.cayenne.dbsync.merge.SetColumnTypeToDb;
+import org.apache.cayenne.dbsync.merge.token.MergerToken;
+import org.apache.cayenne.dbsync.merge.token.SetColumnTypeToDb;
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbEntity;
 

http://git-wip-us.apache.org/repos/asf/cayenne/blob/46c8ded5/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/DefaultMergerTokenFactory.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/DefaultMergerTokenFactory.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/DefaultMergerTokenFactory.java
index c2f96ce..80121fe 100644
--- a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/DefaultMergerTokenFactory.java
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/DefaultMergerTokenFactory.java
@@ -18,29 +18,29 @@
  ****************************************************************/
 package org.apache.cayenne.dbsync.merge.factory;
 
-import org.apache.cayenne.dbsync.merge.AddColumnToDb;
-import org.apache.cayenne.dbsync.merge.AddColumnToModel;
-import org.apache.cayenne.dbsync.merge.AddRelationshipToDb;
-import org.apache.cayenne.dbsync.merge.AddRelationshipToModel;
-import org.apache.cayenne.dbsync.merge.CreateTableToDb;
-import org.apache.cayenne.dbsync.merge.CreateTableToModel;
-import org.apache.cayenne.dbsync.merge.DropColumnToDb;
-import org.apache.cayenne.dbsync.merge.DropColumnToModel;
-import org.apache.cayenne.dbsync.merge.DropRelationshipToDb;
-import org.apache.cayenne.dbsync.merge.DropRelationshipToModel;
-import org.apache.cayenne.dbsync.merge.DropTableToDb;
-import org.apache.cayenne.dbsync.merge.DropTableToModel;
-import org.apache.cayenne.dbsync.merge.MergerToken;
-import org.apache.cayenne.dbsync.merge.SetAllowNullToDb;
-import org.apache.cayenne.dbsync.merge.SetAllowNullToModel;
-import org.apache.cayenne.dbsync.merge.SetColumnTypeToDb;
-import org.apache.cayenne.dbsync.merge.SetColumnTypeToModel;
-import org.apache.cayenne.dbsync.merge.SetNotNullToDb;
-import org.apache.cayenne.dbsync.merge.SetNotNullToModel;
-import org.apache.cayenne.dbsync.merge.SetPrimaryKeyToDb;
-import org.apache.cayenne.dbsync.merge.SetPrimaryKeyToModel;
-import org.apache.cayenne.dbsync.merge.SetValueForNullToDb;
-import org.apache.cayenne.dbsync.merge.ValueForNullProvider;
+import org.apache.cayenne.dbsync.merge.token.AddColumnToDb;
+import org.apache.cayenne.dbsync.merge.token.AddColumnToModel;
+import org.apache.cayenne.dbsync.merge.token.AddRelationshipToDb;
+import org.apache.cayenne.dbsync.merge.token.AddRelationshipToModel;
+import org.apache.cayenne.dbsync.merge.token.CreateTableToDb;
+import org.apache.cayenne.dbsync.merge.token.CreateTableToModel;
+import org.apache.cayenne.dbsync.merge.token.DropColumnToDb;
+import org.apache.cayenne.dbsync.merge.token.DropColumnToModel;
+import org.apache.cayenne.dbsync.merge.token.DropRelationshipToDb;
+import org.apache.cayenne.dbsync.merge.token.DropRelationshipToModel;
+import org.apache.cayenne.dbsync.merge.token.DropTableToDb;
+import org.apache.cayenne.dbsync.merge.token.DropTableToModel;
+import org.apache.cayenne.dbsync.merge.token.MergerToken;
+import org.apache.cayenne.dbsync.merge.token.SetAllowNullToDb;
+import org.apache.cayenne.dbsync.merge.token.SetAllowNullToModel;
+import org.apache.cayenne.dbsync.merge.token.SetColumnTypeToDb;
+import org.apache.cayenne.dbsync.merge.token.SetColumnTypeToModel;
+import org.apache.cayenne.dbsync.merge.token.SetNotNullToDb;
+import org.apache.cayenne.dbsync.merge.token.SetNotNullToModel;
+import org.apache.cayenne.dbsync.merge.token.SetPrimaryKeyToDb;
+import org.apache.cayenne.dbsync.merge.token.SetPrimaryKeyToModel;
+import org.apache.cayenne.dbsync.merge.token.SetValueForNullToDb;
+import org.apache.cayenne.dbsync.merge.token.ValueForNullProvider;
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbEntity;
 import org.apache.cayenne.map.DbRelationship;

http://git-wip-us.apache.org/repos/asf/cayenne/blob/46c8ded5/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/DerbyMergerTokenFactory.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/DerbyMergerTokenFactory.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/DerbyMergerTokenFactory.java
index 398d5cc..8979fa0 100644
--- a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/DerbyMergerTokenFactory.java
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/DerbyMergerTokenFactory.java
@@ -20,10 +20,10 @@ package org.apache.cayenne.dbsync.merge.factory;
 
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.QuotingStrategy;
-import org.apache.cayenne.dbsync.merge.MergerToken;
-import org.apache.cayenne.dbsync.merge.SetAllowNullToDb;
-import org.apache.cayenne.dbsync.merge.SetColumnTypeToDb;
-import org.apache.cayenne.dbsync.merge.SetNotNullToDb;
+import org.apache.cayenne.dbsync.merge.token.MergerToken;
+import org.apache.cayenne.dbsync.merge.token.SetAllowNullToDb;
+import org.apache.cayenne.dbsync.merge.token.SetColumnTypeToDb;
+import org.apache.cayenne.dbsync.merge.token.SetNotNullToDb;
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbEntity;
 

http://git-wip-us.apache.org/repos/asf/cayenne/blob/46c8ded5/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/FirebirdMergerTokenFactory.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/FirebirdMergerTokenFactory.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/FirebirdMergerTokenFactory.java
index 4368977..ce94ba0 100644
--- a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/FirebirdMergerTokenFactory.java
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/FirebirdMergerTokenFactory.java
@@ -21,11 +21,11 @@ package org.apache.cayenne.dbsync.merge.factory;
 
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.QuotingStrategy;
-import org.apache.cayenne.dbsync.merge.AddColumnToDb;
-import org.apache.cayenne.dbsync.merge.DropColumnToDb;
-import org.apache.cayenne.dbsync.merge.MergerToken;
-import org.apache.cayenne.dbsync.merge.SetAllowNullToDb;
-import org.apache.cayenne.dbsync.merge.SetNotNullToDb;
+import org.apache.cayenne.dbsync.merge.token.AddColumnToDb;
+import org.apache.cayenne.dbsync.merge.token.DropColumnToDb;
+import org.apache.cayenne.dbsync.merge.token.MergerToken;
+import org.apache.cayenne.dbsync.merge.token.SetAllowNullToDb;
+import org.apache.cayenne.dbsync.merge.token.SetNotNullToDb;
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbEntity;
 

http://git-wip-us.apache.org/repos/asf/cayenne/blob/46c8ded5/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/H2MergerTokenFactory.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/H2MergerTokenFactory.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/H2MergerTokenFactory.java
index 8acafc3..803921c 100644
--- a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/H2MergerTokenFactory.java
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/H2MergerTokenFactory.java
@@ -21,10 +21,10 @@ package org.apache.cayenne.dbsync.merge.factory;
 
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.QuotingStrategy;
-import org.apache.cayenne.dbsync.merge.MergerToken;
-import org.apache.cayenne.dbsync.merge.SetAllowNullToDb;
-import org.apache.cayenne.dbsync.merge.SetColumnTypeToDb;
-import org.apache.cayenne.dbsync.merge.SetPrimaryKeyToDb;
+import org.apache.cayenne.dbsync.merge.token.MergerToken;
+import org.apache.cayenne.dbsync.merge.token.SetAllowNullToDb;
+import org.apache.cayenne.dbsync.merge.token.SetColumnTypeToDb;
+import org.apache.cayenne.dbsync.merge.token.SetPrimaryKeyToDb;
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbEntity;
 

http://git-wip-us.apache.org/repos/asf/cayenne/blob/46c8ded5/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/HSQLMergerTokenFactory.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/HSQLMergerTokenFactory.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/HSQLMergerTokenFactory.java
index 15cfa18..d168118 100644
--- a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/HSQLMergerTokenFactory.java
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/HSQLMergerTokenFactory.java
@@ -20,10 +20,10 @@ package org.apache.cayenne.dbsync.merge.factory;
 
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.QuotingStrategy;
-import org.apache.cayenne.dbsync.merge.MergerToken;
-import org.apache.cayenne.dbsync.merge.SetAllowNullToDb;
-import org.apache.cayenne.dbsync.merge.SetColumnTypeToDb;
-import org.apache.cayenne.dbsync.merge.SetPrimaryKeyToDb;
+import org.apache.cayenne.dbsync.merge.token.MergerToken;
+import org.apache.cayenne.dbsync.merge.token.SetAllowNullToDb;
+import org.apache.cayenne.dbsync.merge.token.SetColumnTypeToDb;
+import org.apache.cayenne.dbsync.merge.token.SetPrimaryKeyToDb;
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbEntity;
 

http://git-wip-us.apache.org/repos/asf/cayenne/blob/46c8ded5/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/IngresMergerTokenFactory.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/IngresMergerTokenFactory.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/IngresMergerTokenFactory.java
index 19d2860..4dc715e 100644
--- a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/IngresMergerTokenFactory.java
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/IngresMergerTokenFactory.java
@@ -20,13 +20,13 @@ package org.apache.cayenne.dbsync.merge.factory;
 
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.QuotingStrategy;
-import org.apache.cayenne.dbsync.merge.AddRelationshipToDb;
-import org.apache.cayenne.dbsync.merge.DropColumnToDb;
-import org.apache.cayenne.dbsync.merge.DropRelationshipToDb;
-import org.apache.cayenne.dbsync.merge.MergerToken;
-import org.apache.cayenne.dbsync.merge.SetAllowNullToDb;
-import org.apache.cayenne.dbsync.merge.SetColumnTypeToDb;
-import org.apache.cayenne.dbsync.merge.SetNotNullToDb;
+import org.apache.cayenne.dbsync.merge.token.AddRelationshipToDb;
+import org.apache.cayenne.dbsync.merge.token.DropColumnToDb;
+import org.apache.cayenne.dbsync.merge.token.DropRelationshipToDb;
+import org.apache.cayenne.dbsync.merge.token.MergerToken;
+import org.apache.cayenne.dbsync.merge.token.SetAllowNullToDb;
+import org.apache.cayenne.dbsync.merge.token.SetColumnTypeToDb;
+import org.apache.cayenne.dbsync.merge.token.SetNotNullToDb;
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbEntity;
 import org.apache.cayenne.map.DbJoin;

http://git-wip-us.apache.org/repos/asf/cayenne/blob/46c8ded5/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/MergerTokenFactory.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/MergerTokenFactory.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/MergerTokenFactory.java
index 46b6ef3..c61f1ed 100644
--- a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/MergerTokenFactory.java
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/MergerTokenFactory.java
@@ -18,8 +18,8 @@
  ****************************************************************/
 package org.apache.cayenne.dbsync.merge.factory;
 
-import org.apache.cayenne.dbsync.merge.MergerToken;
-import org.apache.cayenne.dbsync.merge.ValueForNullProvider;
+import org.apache.cayenne.dbsync.merge.token.MergerToken;
+import org.apache.cayenne.dbsync.merge.token.ValueForNullProvider;
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbEntity;
 import org.apache.cayenne.map.DbRelationship;

http://git-wip-us.apache.org/repos/asf/cayenne/blob/46c8ded5/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/MySQLMergerTokenFactory.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/MySQLMergerTokenFactory.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/MySQLMergerTokenFactory.java
index 2193446..1d4ab9f 100644
--- a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/MySQLMergerTokenFactory.java
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/MySQLMergerTokenFactory.java
@@ -20,12 +20,12 @@ package org.apache.cayenne.dbsync.merge.factory;
 
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.QuotingStrategy;
-import org.apache.cayenne.dbsync.merge.DropRelationshipToDb;
-import org.apache.cayenne.dbsync.merge.MergerToken;
-import org.apache.cayenne.dbsync.merge.SetAllowNullToDb;
-import org.apache.cayenne.dbsync.merge.SetColumnTypeToDb;
-import org.apache.cayenne.dbsync.merge.SetNotNullToDb;
-import org.apache.cayenne.dbsync.merge.SetPrimaryKeyToDb;
+import org.apache.cayenne.dbsync.merge.token.DropRelationshipToDb;
+import org.apache.cayenne.dbsync.merge.token.MergerToken;
+import org.apache.cayenne.dbsync.merge.token.SetAllowNullToDb;
+import org.apache.cayenne.dbsync.merge.token.SetColumnTypeToDb;
+import org.apache.cayenne.dbsync.merge.token.SetNotNullToDb;
+import org.apache.cayenne.dbsync.merge.token.SetPrimaryKeyToDb;
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbEntity;
 import org.apache.cayenne.map.DbRelationship;

http://git-wip-us.apache.org/repos/asf/cayenne/blob/46c8ded5/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/OpenBaseMergerTokenFactory.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/OpenBaseMergerTokenFactory.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/OpenBaseMergerTokenFactory.java
index 7235f6b..19fa0b9 100644
--- a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/OpenBaseMergerTokenFactory.java
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/OpenBaseMergerTokenFactory.java
@@ -19,12 +19,12 @@
 package org.apache.cayenne.dbsync.merge.factory;
 
 import org.apache.cayenne.dba.DbAdapter;
-import org.apache.cayenne.dbsync.merge.CreateTableToDb;
-import org.apache.cayenne.dbsync.merge.DropRelationshipToDb;
-import org.apache.cayenne.dbsync.merge.MergerToken;
-import org.apache.cayenne.dbsync.merge.SetAllowNullToDb;
-import org.apache.cayenne.dbsync.merge.SetColumnTypeToDb;
-import org.apache.cayenne.dbsync.merge.SetNotNullToDb;
+import org.apache.cayenne.dbsync.merge.token.CreateTableToDb;
+import org.apache.cayenne.dbsync.merge.token.DropRelationshipToDb;
+import org.apache.cayenne.dbsync.merge.token.MergerToken;
+import org.apache.cayenne.dbsync.merge.token.SetAllowNullToDb;
+import org.apache.cayenne.dbsync.merge.token.SetColumnTypeToDb;
+import org.apache.cayenne.dbsync.merge.token.SetNotNullToDb;
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbEntity;
 import org.apache.cayenne.map.DbJoin;

http://git-wip-us.apache.org/repos/asf/cayenne/blob/46c8ded5/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/OracleMergerTokenFactory.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/OracleMergerTokenFactory.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/OracleMergerTokenFactory.java
index 2c4032b..865e5a7 100644
--- a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/OracleMergerTokenFactory.java
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/OracleMergerTokenFactory.java
@@ -20,11 +20,11 @@ package org.apache.cayenne.dbsync.merge.factory;
 
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.QuotingStrategy;
-import org.apache.cayenne.dbsync.merge.AddColumnToDb;
-import org.apache.cayenne.dbsync.merge.MergerToken;
-import org.apache.cayenne.dbsync.merge.SetAllowNullToDb;
-import org.apache.cayenne.dbsync.merge.SetColumnTypeToDb;
-import org.apache.cayenne.dbsync.merge.SetNotNullToDb;
+import org.apache.cayenne.dbsync.merge.token.AddColumnToDb;
+import org.apache.cayenne.dbsync.merge.token.MergerToken;
+import org.apache.cayenne.dbsync.merge.token.SetAllowNullToDb;
+import org.apache.cayenne.dbsync.merge.token.SetColumnTypeToDb;
+import org.apache.cayenne.dbsync.merge.token.SetNotNullToDb;
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbEntity;
 

http://git-wip-us.apache.org/repos/asf/cayenne/blob/46c8ded5/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/PostgresMergerTokenFactory.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/PostgresMergerTokenFactory.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/PostgresMergerTokenFactory.java
index 935ecfb..a14b99d 100644
--- a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/PostgresMergerTokenFactory.java
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/PostgresMergerTokenFactory.java
@@ -21,8 +21,8 @@ package org.apache.cayenne.dbsync.merge.factory;
 import org.apache.cayenne.dba.QuotingStrategy;
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbEntity;
-import org.apache.cayenne.dbsync.merge.MergerToken;
-import org.apache.cayenne.dbsync.merge.SetColumnTypeToDb;
+import org.apache.cayenne.dbsync.merge.token.MergerToken;
+import org.apache.cayenne.dbsync.merge.token.SetColumnTypeToDb;
 
 public class PostgresMergerTokenFactory extends DefaultMergerTokenFactory {
 

http://git-wip-us.apache.org/repos/asf/cayenne/blob/46c8ded5/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/SQLServerMergerTokenFactory.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/SQLServerMergerTokenFactory.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/SQLServerMergerTokenFactory.java
index 768b957..00673ad 100644
--- a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/SQLServerMergerTokenFactory.java
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/SQLServerMergerTokenFactory.java
@@ -20,11 +20,11 @@ package org.apache.cayenne.dbsync.merge.factory;
 
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.QuotingStrategy;
-import org.apache.cayenne.dbsync.merge.AddColumnToDb;
-import org.apache.cayenne.dbsync.merge.MergerToken;
-import org.apache.cayenne.dbsync.merge.SetAllowNullToDb;
-import org.apache.cayenne.dbsync.merge.SetColumnTypeToDb;
-import org.apache.cayenne.dbsync.merge.SetNotNullToDb;
+import org.apache.cayenne.dbsync.merge.token.AddColumnToDb;
+import org.apache.cayenne.dbsync.merge.token.MergerToken;
+import org.apache.cayenne.dbsync.merge.token.SetAllowNullToDb;
+import org.apache.cayenne.dbsync.merge.token.SetColumnTypeToDb;
+import org.apache.cayenne.dbsync.merge.token.SetNotNullToDb;
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbEntity;
 

http://git-wip-us.apache.org/repos/asf/cayenne/blob/46c8ded5/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/SybaseMergerTokenFactory.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/SybaseMergerTokenFactory.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/SybaseMergerTokenFactory.java
index f295305..48b5a22 100644
--- a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/SybaseMergerTokenFactory.java
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/SybaseMergerTokenFactory.java
@@ -20,12 +20,12 @@ package org.apache.cayenne.dbsync.merge.factory;
 
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.QuotingStrategy;
-import org.apache.cayenne.dbsync.merge.AddColumnToDb;
-import org.apache.cayenne.dbsync.merge.DropColumnToDb;
-import org.apache.cayenne.dbsync.merge.MergerToken;
-import org.apache.cayenne.dbsync.merge.SetAllowNullToDb;
-import org.apache.cayenne.dbsync.merge.SetColumnTypeToDb;
-import org.apache.cayenne.dbsync.merge.SetNotNullToDb;
+import org.apache.cayenne.dbsync.merge.token.AddColumnToDb;
+import org.apache.cayenne.dbsync.merge.token.DropColumnToDb;
+import org.apache.cayenne.dbsync.merge.token.MergerToken;
+import org.apache.cayenne.dbsync.merge.token.SetAllowNullToDb;
+import org.apache.cayenne.dbsync.merge.token.SetColumnTypeToDb;
+import org.apache.cayenne.dbsync.merge.token.SetNotNullToDb;
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbEntity;
 

http://git-wip-us.apache.org/repos/asf/cayenne/blob/46c8ded5/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/token/AbstractToDbToken.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/token/AbstractToDbToken.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/token/AbstractToDbToken.java
new file mode 100644
index 0000000..b054ddb
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/token/AbstractToDbToken.java
@@ -0,0 +1,133 @@
+/*****************************************************************
+ *   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.token;
+
+import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.dbsync.merge.context.MergeDirection;
+import org.apache.cayenne.dbsync.merge.context.MergerContext;
+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/token/AbstractToModelToken.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/token/AbstractToModelToken.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/token/AbstractToModelToken.java
new file mode 100644
index 0000000..f61067e
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/token/AbstractToModelToken.java
@@ -0,0 +1,128 @@
+/*****************************************************************
+ *   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.token;
+
+import org.apache.cayenne.dbsync.merge.context.MergeDirection;
+import org.apache.cayenne.dbsync.reverse.dbload.ModelMergeDelegate;
+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/token/AddColumnToDb.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/token/AddColumnToDb.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/token/AddColumnToDb.java
new file mode 100644
index 0000000..19ac1ce
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/token/AddColumnToDb.java
@@ -0,0 +1,74 @@
+/*****************************************************************
+ *   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.token;
+
+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/token/AddColumnToModel.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/token/AddColumnToModel.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/token/AddColumnToModel.java
new file mode 100644
index 0000000..e68d271
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/token/AddColumnToModel.java
@@ -0,0 +1,55 @@
+/*****************************************************************
+ *   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.token;
+
+import org.apache.cayenne.dbsync.merge.context.EntityMergeSupport;
+import org.apache.cayenne.dbsync.merge.context.MergerContext;
+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/token/AddRelationshipToDb.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/token/AddRelationshipToDb.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/token/AddRelationshipToDb.java
new file mode 100644
index 0000000..2d19422
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/token/AddRelationshipToDb.java
@@ -0,0 +1,88 @@
+/*****************************************************************
+ *   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.token;
+
+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/token/AddRelationshipToModel.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/token/AddRelationshipToModel.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/token/AddRelationshipToModel.java
new file mode 100644
index 0000000..c132bbc
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/token/AddRelationshipToModel.java
@@ -0,0 +1,91 @@
+/*****************************************************************
+ *   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.token;
+
+import org.apache.cayenne.dbsync.merge.context.MergerContext;
+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;
+    }
+}