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/09/29 17:39:00 UTC

[14/15] cayenne git commit: CAY-2116 Split schema synchronization code in a separate module

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DropColumnToModel.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DropColumnToModel.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DropColumnToModel.java
new file mode 100644
index 0000000..ceb03bc
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DropColumnToModel.java
@@ -0,0 +1,75 @@
+/*****************************************************************
+ *   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.List;
+
+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.DbJoin;
+import org.apache.cayenne.map.DbRelationship;
+import org.apache.cayenne.map.ObjAttribute;
+import org.apache.cayenne.map.ObjEntity;
+
+/**
+ * A {@link MergerToken} to remove a {@link DbAttribute} from a {@link DbEntity}.
+ * 
+ */
+public class DropColumnToModel extends AbstractToModelToken.EntityAndColumn {
+
+    public DropColumnToModel(DbEntity entity, DbAttribute column) {
+        super("Drop Column", entity, column);
+    }
+
+    public MergerToken createReverse(MergerTokenFactory factory) {
+        return factory.createAddColumnToDb(getEntity(), getColumn());
+    }
+
+    public void execute(MergerContext mergerContext) {
+
+        // remove relationships mapped to column. duplicate List to prevent
+        // ConcurrentModificationException
+        List<DbRelationship> dbRelationships = new ArrayList<DbRelationship>(getEntity()
+                .getRelationships());
+        for (DbRelationship dbRelationship : dbRelationships) {
+            for (DbJoin join : dbRelationship.getJoins()) {
+                if (join.getSource() == getColumn() || join.getTarget() == getColumn()) {
+                    remove(mergerContext.getModelMergeDelegate(), dbRelationship, true);
+                }
+            }
+        }
+
+        // remove ObjAttribute mapped to same column
+        for (ObjEntity objEntity : getEntity().mappedObjEntities()) {
+            ObjAttribute objAttribute = objEntity.getAttributeForDbAttribute(getColumn());
+            if (objAttribute != null) {
+                objEntity.removeAttribute(objAttribute.getName());
+                mergerContext.getModelMergeDelegate().objAttributeRemoved(objAttribute);
+            }
+
+        }
+
+        // remove DbAttribute
+        getEntity().removeAttribute(getColumn().getName());
+
+        mergerContext.getModelMergeDelegate().dbAttributeRemoved(getColumn());
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DropRelationshipToDb.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DropRelationshipToDb.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DropRelationshipToDb.java
new file mode 100644
index 0000000..238a44a
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DropRelationshipToDb.java
@@ -0,0 +1,72 @@
+/*****************************************************************
+ *   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.DbEntity;
+import org.apache.cayenne.map.DbRelationship;
+import org.apache.cayenne.map.DbRelationshipDetected;
+
+public class DropRelationshipToDb extends AbstractToDbToken.Entity {
+
+    private DbRelationship rel;
+
+    public DropRelationshipToDb(DbEntity entity, DbRelationship rel) {
+        super("Drop foreign key", entity);
+        this.rel = rel;
+    }
+    
+    public String getFkName() {
+        if (rel instanceof DbRelationshipDetected) {
+            return ((DbRelationshipDetected) rel).getFkName();
+        }
+        return null;
+    }
+
+    @Override
+    public List<String> createSql(DbAdapter adapter) {
+        String fkName = getFkName();
+        if (fkName == null) {
+            return Collections.emptyList();
+        }
+
+        QuotingStrategy context = adapter.getQuotingStrategy();
+        return Collections.singletonList(
+                "ALTER TABLE " + context.quotedFullyQualifiedName(getEntity()) + " DROP CONSTRAINT " + fkName);
+    }
+
+    public MergerToken createReverse(MergerTokenFactory factory) {
+        return factory.createAddRelationshipToModel(getEntity(), rel);
+    }
+
+    @Override
+    public String getTokenValue() {
+        return rel.getSourceEntity().getName() + "->" + rel.getTargetEntityName();
+    }
+    
+    public DbRelationship getRelationship() {
+        return rel;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DropRelationshipToModel.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DropRelationshipToModel.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DropRelationshipToModel.java
new file mode 100644
index 0000000..686df6e
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DropRelationshipToModel.java
@@ -0,0 +1,51 @@
+/*****************************************************************
+ *   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.DbRelationship;
+
+public class DropRelationshipToModel extends AbstractToModelToken.Entity {
+
+    private final DbRelationship rel;
+
+    public DropRelationshipToModel(DbEntity entity, DbRelationship rel) {
+        super("Drop db-relationship ", entity);
+        this.rel = rel;
+    }
+
+    public MergerToken createReverse(MergerTokenFactory factory) {
+        return factory.createAddRelationshipToDb(getEntity(), rel);
+    }
+
+    public void execute(MergerContext mergerContext) {
+        remove(mergerContext.getModelMergeDelegate(), rel, true);
+    }
+
+    @Override
+    public String getTokenValue() {
+        return AddRelationshipToModel.getTokenValue(rel);
+    }
+    
+    public DbRelationship getRelationship() {
+        return rel;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DropTableToDb.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DropTableToDb.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DropTableToDb.java
new file mode 100644
index 0000000..6b76646
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DropTableToDb.java
@@ -0,0 +1,50 @@
+/*****************************************************************
+ *   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.List;
+
+import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.dbsync.merge.factory.MergerTokenFactory;
+import org.apache.cayenne.map.DbEntity;
+
+public class DropTableToDb extends AbstractToDbToken.Entity {
+
+    public DropTableToDb(DbEntity entity) {
+        super("Drop Table", entity);
+    }
+
+    @Override
+    public List<String> createSql(DbAdapter adapter) {
+        List<String> sqls = new ArrayList<String>();
+        // TODO: fix. some adapters drop the complete AUTO_PK_SUPPORT here
+        /*
+        sqls.addAll(adapter.getPkGenerator().dropAutoPkStatements(
+                Collections.singletonList(entity)));
+         */
+        sqls.addAll(adapter.dropTableStatements(getEntity()));
+        return sqls;
+    }
+
+    public MergerToken createReverse(MergerTokenFactory factory) {
+        return factory.createCreateTableToModel(getEntity());
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DropTableToModel.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DropTableToModel.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DropTableToModel.java
new file mode 100644
index 0000000..7a47f37
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DropTableToModel.java
@@ -0,0 +1,49 @@
+/*****************************************************************
+ *   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.DataMap;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.ObjEntity;
+
+/**
+ * A {@link MergerToken} to remove a {@link DbEntity} from a {@link DataMap}. Any
+ * {@link ObjEntity} mapped to the {@link DbEntity} will also be removed.
+ * 
+ */
+public class DropTableToModel extends AbstractToModelToken.Entity {
+
+    public DropTableToModel(DbEntity entity) {
+        super("Drop Table", entity);
+    }
+
+    public MergerToken createReverse(MergerTokenFactory factory) {
+        return factory.createCreateTableToDb(getEntity());
+    }
+
+    public void execute(MergerContext mergerContext) {
+        for (ObjEntity objEntity : getEntity().mappedObjEntities()) {
+            objEntity.getDataMap().removeObjEntity(objEntity.getName(), true);
+            mergerContext.getModelMergeDelegate().objEntityRemoved(objEntity);
+        }
+        getEntity().getDataMap().removeDbEntity(getEntity().getName(), true);
+        mergerContext.getModelMergeDelegate().dbEntityRemoved(getEntity());
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DummyReverseToken.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DummyReverseToken.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DummyReverseToken.java
new file mode 100644
index 0000000..54b779b
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DummyReverseToken.java
@@ -0,0 +1,60 @@
+/*****************************************************************
+ *   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;
+
+/**
+ * The reverse of a {@link MergerToken} that can not be reversed.. This will not execute
+ * any thing, but {@link #createReverse(MergerTokenFactory)} will get back the reverse that
+ * this was made from.
+ */
+class DummyReverseToken implements MergerToken {
+
+    private MergerToken reverse;
+
+    public DummyReverseToken(MergerToken reverse) {
+        this.reverse = reverse;
+    }
+
+    public MergerToken createReverse(MergerTokenFactory factory) {
+        return reverse;
+    }
+
+    public void execute(MergerContext mergerContext) {
+        // can not execute
+    }
+
+    public MergeDirection getDirection() {
+        return reverse.getDirection().reverseDirection();
+    }
+
+    public String getTokenName() {
+        return "Can not execute the reverse of " + reverse.getTokenName();
+    }
+
+    public String getTokenValue() {
+        return reverse.getTokenValue();
+    }
+
+    public boolean isReversible() {
+        return true;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/EmptyValueForNullProvider.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/EmptyValueForNullProvider.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/EmptyValueForNullProvider.java
new file mode 100644
index 0000000..a484655
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/EmptyValueForNullProvider.java
@@ -0,0 +1,40 @@
+/*****************************************************************
+ *   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.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+
+/**
+ * A dummy {@link ValueForNullProvider} that are not able to provide any values
+ */
+class EmptyValueForNullProvider implements ValueForNullProvider {
+
+    public List<String> createSql(DbEntity entity, DbAttribute column) {
+        return Collections.emptyList();
+    }
+
+    public boolean hasValueFor(DbEntity entity, DbAttribute column) {
+        return false;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/EntityMergeSupport.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/EntityMergeSupport.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/EntityMergeSupport.java
new file mode 100644
index 0000000..8aea02a
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/EntityMergeSupport.java
@@ -0,0 +1,522 @@
+/*****************************************************************
+ *   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.TypesMapping;
+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.naming.DefaultUniqueNameGenerator;
+import org.apache.cayenne.map.naming.LegacyNameGenerator;
+import org.apache.cayenne.map.naming.NameCheckers;
+import org.apache.cayenne.map.naming.ObjectNameGenerator;
+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 LOG = 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 DataMap map;
+    /**
+     * Strategy for choosing names for entities, attributes and relationships
+     */
+    private final ObjectNameGenerator nameGenerator;
+    /**
+     * Listeners of merge process.
+     */
+    private final List<EntityMergeListener> listeners = new ArrayList<EntityMergeListener>();
+    protected boolean removeMeaningfulFKs;
+    protected boolean removeMeaningfulPKs;
+    protected boolean usePrimitives;
+
+    public EntityMergeSupport(DataMap map) {
+        this(map, new LegacyNameGenerator(), true);
+    }
+
+    /**
+     * @since 3.0
+     */
+    public EntityMergeSupport(DataMap map, ObjectNameGenerator nameGenerator, boolean removeMeaningfulPKs) {
+        this.map = map;
+        this.nameGenerator = nameGenerator;
+        this.removeMeaningfulFKs = true;
+        this.removeMeaningfulPKs = removeMeaningfulPKs;
+
+        /**
+         * Adding a listener, so that all created ObjRelationships would have
+         * default delete rule
+         */
+        addEntityMergeListener(DeleteRuleUpdater.getEntityMergeListener());
+    }
+
+    /**
+     * 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.
+     * @since 1.2 changed signature to use Collection instead of List.
+     */
+    public boolean synchronizeWithDbEntities(Iterable<ObjEntity> objEntities) {
+        boolean changed = false;
+        for (ObjEntity nextEntity : objEntities) {
+            if (synchronizeWithDbEntity(nextEntity)) {
+                changed = true;
+            }
+        }
+
+        return changed;
+    }
+
+    /**
+     * @since 4.0
+     */
+    protected boolean removePK(DbEntity dbEntity) {
+        return removeMeaningfulPKs;
+    }
+
+    /**
+     * @since 4.0
+     */
+    protected boolean removeFK(DbEntity dbEntity) {
+        return removeMeaningfulFKs;
+    }
+
+    /**
+     * 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;
+
+        // synchronization on DataMap is some (weak) protection
+        // against simultaneous modification of the map (like double-clicking on sync button)
+        synchronized (map) {
+
+            if (removeFK(dbEntity)) {
+                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 (isMissingFromObjEntity(entity, dbAttribute, incomingRels)) {
+            addMissingAttribute(entity, dbAttribute);
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * @since 4.0
+     */
+    public boolean synchronizeOnDbRelationshipAdded(ObjEntity entity, DbRelationship dbRelationship) {
+
+        if (isMissingFromObjEntity(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) {
+        String relationshipName = nameGenerator.createObjRelationshipName(dr);
+        relationshipName = DefaultUniqueNameGenerator.generate(NameCheckers.objRelationship, entity, relationshipName);
+
+        ObjRelationship or = new ObjRelationship(relationshipName);
+        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) {
+        DbEntity targetEntity = dbRelationship.getTargetEntity();
+
+        Collection<ObjEntity> mappedObjEntities = map.getMappedEntities(targetEntity);
+        if (!mappedObjEntities.isEmpty()) {
+            for (Entity mappedTarget : mappedObjEntities) {
+                createObjRelationship(entity, dbRelationship, mappedTarget.getName());
+            }
+        } else {
+
+            if (targetEntity == null) {
+                targetEntity = new DbEntity(dbRelationship.getTargetEntityName());
+            }
+
+            if (dbRelationship.getTargetEntityName() != null) {
+                boolean needGeneratedEntity = createObjRelationship(entity, dbRelationship,
+                        nameGenerator.createObjEntityName(targetEntity));
+                if (needGeneratedEntity) {
+                    LOG.warn("Can't find ObjEntity for " + dbRelationship.getTargetEntityName());
+                    LOG.warn("Db Relationship (" + dbRelationship + ") will have GUESSED Obj Relationship reflection. ");
+                }
+            }
+        }
+    }
+
+    private void addMissingAttribute(ObjEntity entity, DbAttribute da) {
+        String attrName = DefaultUniqueNameGenerator.generate(NameCheckers.objAttribute, entity,
+                nameGenerator.createObjAttributeName(da));
+
+        String type = TypesMapping.getJavaBySqlType(da.getType());
+        if (usePrimitives) {
+            String primitive = CLASS_TO_PRIMITIVE.get(type);
+            if (primitive != null) {
+                type = primitive;
+            }
+        }
+
+        ObjAttribute oa = new ObjAttribute(attrName, type, entity);
+        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<DbAttribute>(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<DbAttribute>();
+        Collection<DbRelationship> incomingRels = getIncomingRelationships(dbEntity);
+
+        for (DbAttribute dba : dbEntity.getAttributes()) {
+
+            if (isMissingFromObjEntity(objEntity, dba, incomingRels)) {
+                missing.add(dba);
+            }
+        }
+
+        return missing;
+    }
+
+    protected boolean isMissingFromObjEntity(ObjEntity entity, DbAttribute dbAttribute, Collection<DbRelationship> incomingRels) {
+
+        if (dbAttribute.getName() == null || entity.getAttributeForDbAttribute(dbAttribute) != null) {
+            return false;
+        }
+
+        boolean removeMeaningfulPKs = removePK(dbAttribute.getEntity());
+        if (removeMeaningfulPKs && dbAttribute.isPrimaryKey()) {
+            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 (!removeMeaningfulPKs) {
+            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 (!removeMeaningfulPKs) {
+            if (!dbAttribute.isPrimaryKey() && isFK) {
+                return false;
+            }
+        } else {
+            if (isFK) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    protected boolean isMissingFromObjEntity(ObjEntity entity, DbRelationship dbRelationship) {
+        return dbRelationship.getName() != null && entity.getRelationshipForDbRelationship(dbRelationship) == null;
+    }
+
+    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 (isMissingFromObjEntity(objEntity, dbRel)) {
+                missing.add(dbRel);
+            }
+        }
+
+        return missing;
+    }
+
+    /**
+     * @since 1.2
+     */
+    public boolean isRemoveMeaningfulFKs() {
+        return removeMeaningfulFKs;
+    }
+
+    /**
+     * @since 1.2
+     */
+    public void setRemoveMeaningfulFKs(boolean removeMeaningfulFKs) {
+        this.removeMeaningfulFKs = removeMeaningfulFKs;
+    }
+
+    /**
+     * Registers new EntityMergeListener
+     */
+    public void addEntityMergeListener(EntityMergeListener listener) {
+        listeners.add(listener);
+    }
+
+    /**
+     * Unregisters an EntityMergeListener
+     */
+    public void removeEntityMergeListener(EntityMergeListener listener) {
+        listeners.remove(listener);
+    }
+
+    /**
+     * Returns registered listeners
+     */
+    public EntityMergeListener[] getEntityMergeListeners() {
+        return listeners.toArray(new EntityMergeListener[listeners.size()]);
+    }
+
+    /**
+     * 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);
+        }
+    }
+
+    /**
+     * @return naming strategy for reverse engineering
+     */
+    public ObjectNameGenerator getNameGenerator() {
+        return nameGenerator;
+    }
+
+    /**
+     * @since 4.0
+     */
+    public boolean isUsePrimitives() {
+        return usePrimitives;
+    }
+
+    /**
+     * @param usePrimitives
+     * @since 4.0
+     */
+    public void setUsePrimitives(boolean usePrimitives) {
+        this.usePrimitives = usePrimitives;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/MergeDirection.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/MergeDirection.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/MergeDirection.java
new file mode 100644
index 0000000..4ac25e4
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/MergeDirection.java
@@ -0,0 +1,70 @@
+/*****************************************************************
+ *   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;
+
+/**
+ * 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/2f7b1d53/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/MergerContext.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/MergerContext.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/MergerContext.java
new file mode 100644
index 0000000..772de97
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/MergerContext.java
@@ -0,0 +1,118 @@
+/*****************************************************************
+ *   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.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;
+
+    protected MergerContext() {
+    }
+
+    public static Builder builder(DataMap dataMap) {
+        return new Builder().dataMap(dataMap);
+    }
+
+    /**
+     * @deprecated since 4.0 use {@link #getDataNode()} and its {@link DataNode#getAdapter()} method.
+     */
+    @Deprecated
+    public DbAdapter getAdapter() {
+        return getDataNode().getAdapter();
+    }
+
+    /**
+     * 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.
+     */
+    public ModelMergeDelegate getModelMergeDelegate() {
+        return delegate;
+    }
+
+    public static class Builder {
+
+        private MergerContext context;
+
+        private Builder() {
+            this.context = new MergerContext();
+            this.context.validationResult = new ValidationResult();
+            this.context.delegate = new DefaultModelMergeDelegate();
+            this.context.dataNode = new DataNode();
+        }
+
+        public MergerContext build() {
+            return context;
+        }
+
+        public Builder delegate(ModelMergeDelegate delegate) {
+            context.delegate = Objects.requireNonNull(delegate);
+            return this;
+        }
+
+        public Builder dataNode(DataNode dataNode) {
+            this.context.dataNode = Objects.requireNonNull(dataNode);
+            return this;
+        }
+
+        public Builder syntheticDataNode(DataSource dataSource, DbAdapter adapter) {
+            DataNode dataNode = new DataNode();
+            dataNode.setDataSource(dataSource);
+            dataNode.setAdapter(adapter);
+            return dataNode(dataNode);
+        }
+
+        public Builder dataMap(DataMap dataMap) {
+            context.dataMap = Objects.requireNonNull(dataMap);
+            return this;
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/MergerToken.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/MergerToken.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/MergerToken.java
new file mode 100644
index 0000000..5359b62
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/MergerToken.java
@@ -0,0 +1,53 @@
+/*****************************************************************
+ *   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;
+
+/**
+ * Represents a minimal atomic synchronization operation between database and Cayenne model.
+ */
+public interface MergerToken {
+
+    String getTokenName();
+
+    String getTokenValue();
+
+    /**
+     * The direction of this token. One of {@link MergeDirection#TO_DB} or
+     * {@link MergeDirection#TO_MODEL}
+     */
+    MergeDirection getDirection();
+
+    /**
+     * Create a complimentary token with the reverse direction. AddColumn in one direction becomes
+     * DropColumn in the other direction.
+     * <p>
+     * Not all tokens are reversible.
+     */
+    MergerToken createReverse(MergerTokenFactory factory);
+
+    /**
+     * Executes synchronization operation.
+     *
+     * @param mergerContext operation context.
+     */
+    void execute(MergerContext mergerContext);
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/ModelMergeDelegate.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/ModelMergeDelegate.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/ModelMergeDelegate.java
new file mode 100644
index 0000000..13f6ea5
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/ModelMergeDelegate.java
@@ -0,0 +1,62 @@
+/*****************************************************************
+ *   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 interface used to tell about modifications performed on the model by
+ * {@link MergerToken} with {@link MergeDirection#TO_MODEL}
+ */
+public interface ModelMergeDelegate {
+
+    public void dbEntityAdded(DbEntity ent);
+
+    public void dbEntityRemoved(DbEntity ent);
+
+    public void objEntityAdded(ObjEntity ent);
+
+    public void objEntityRemoved(ObjEntity ent);
+
+    public void dbAttributeAdded(DbAttribute att);
+
+    public void dbAttributeRemoved(DbAttribute att);
+
+    public void dbAttributeModified(DbAttribute att);
+
+    public void objAttributeAdded(ObjAttribute att);
+
+    public void objAttributeRemoved(ObjAttribute att);
+
+    public void objAttributeModified(ObjAttribute att);
+
+    public void dbRelationshipAdded(DbRelationship rel);
+
+    public void dbRelationshipRemoved(DbRelationship rel);
+
+    public void objRelationshipAdded(ObjRelationship rel);
+
+    public void objRelationshipRemoved(ObjRelationship rel);
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/ProxyModelMergeDelegate.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/ProxyModelMergeDelegate.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/ProxyModelMergeDelegate.java
new file mode 100644
index 0000000..ba12824
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/ProxyModelMergeDelegate.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 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;
+
+/**
+ * @since 4.0
+ */
+public class ProxyModelMergeDelegate implements ModelMergeDelegate {
+
+    private final ModelMergeDelegate delegate;
+
+    public ProxyModelMergeDelegate(ModelMergeDelegate delegate) {
+        this.delegate = delegate;
+    }
+
+    @Override
+    public void dbEntityAdded(DbEntity ent) {
+        delegate.dbEntityAdded(ent);
+    }
+
+    @Override
+    public void dbEntityRemoved(DbEntity ent) {
+        delegate.dbEntityRemoved(ent);
+    }
+
+    @Override
+    public void objEntityAdded(ObjEntity ent) {
+        delegate.objEntityAdded(ent);
+    }
+
+    @Override
+    public void objEntityRemoved(ObjEntity ent) {
+        delegate.objEntityRemoved(ent);
+    }
+
+    @Override
+    public void dbAttributeAdded(DbAttribute att) {
+        delegate.dbAttributeAdded(att);
+    }
+
+    @Override
+    public void dbAttributeRemoved(DbAttribute att) {
+        delegate.dbAttributeRemoved(att);
+    }
+
+    @Override
+    public void dbAttributeModified(DbAttribute att) {
+        delegate.dbAttributeModified(att);
+    }
+
+    @Override
+    public void objAttributeAdded(ObjAttribute att) {
+        delegate.objAttributeAdded(att);
+    }
+
+    @Override
+    public void objAttributeRemoved(ObjAttribute att) {
+        delegate.objAttributeRemoved(att);
+    }
+
+    @Override
+    public void objAttributeModified(ObjAttribute att) {
+        delegate.objAttributeModified(att);
+    }
+
+    @Override
+    public void dbRelationshipAdded(DbRelationship rel) {
+        delegate.dbRelationshipAdded(rel);
+    }
+
+    @Override
+    public void dbRelationshipRemoved(DbRelationship rel) {
+        delegate.dbRelationshipRemoved(rel);
+    }
+
+    @Override
+    public void objRelationshipAdded(ObjRelationship rel) {
+        delegate.objRelationshipAdded(rel);
+    }
+
+    @Override
+    public void objRelationshipRemoved(ObjRelationship rel) {
+        delegate.objRelationshipRemoved(rel);
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/SetAllowNullToDb.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/SetAllowNullToDb.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/SetAllowNullToDb.java
new file mode 100644
index 0000000..c69fb6f
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/SetAllowNullToDb.java
@@ -0,0 +1,57 @@
+/*****************************************************************
+ *   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;
+
+/**
+ * A {@link MergerToken} to add a "allow null" clause to a column.
+ * 
+ */
+public class SetAllowNullToDb extends AbstractToDbToken.EntityAndColumn {
+
+    public SetAllowNullToDb(DbEntity entity, DbAttribute column) {
+        super("Set Allow Null", 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(" ALTER COLUMN ");
+        sqlBuffer.append(context.quotedName(getColumn()));
+        sqlBuffer.append(" DROP NOT NULL");
+
+        return Collections.singletonList(sqlBuffer.toString());
+    }
+
+    public MergerToken createReverse(MergerTokenFactory factory) {
+        return factory.createSetNotNullToModel(getEntity(), getColumn());
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/SetAllowNullToModel.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/SetAllowNullToModel.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/SetAllowNullToModel.java
new file mode 100644
index 0000000..71a0937
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/SetAllowNullToModel.java
@@ -0,0 +1,43 @@
+/*****************************************************************
+ *   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;
+
+/**
+ * A {@link MergerToken} to set the mandatory field of a {@link DbAttribute} to false
+ * 
+ */
+public class SetAllowNullToModel extends AbstractToModelToken.EntityAndColumn {
+
+    public SetAllowNullToModel(DbEntity entity, DbAttribute column) {
+        super("Set Allow Null", entity, column);
+    }
+
+    public MergerToken createReverse(MergerTokenFactory factory) {
+        return factory.createSetNotNullToDb(getEntity(), getColumn());
+    }
+
+    public void execute(MergerContext mergerContext) {
+        getColumn().setMandatory(false);
+        mergerContext.getModelMergeDelegate().dbAttributeModified(getColumn());
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/SetColumnTypeToDb.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/SetColumnTypeToDb.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/SetColumnTypeToDb.java
new file mode 100644
index 0000000..74a0034
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/SetColumnTypeToDb.java
@@ -0,0 +1,119 @@
+/*
+ * 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.JdbcAdapter;
+import org.apache.cayenne.dba.QuotingStrategy;
+import org.apache.cayenne.dba.TypesMapping;
+import org.apache.cayenne.dbsync.merge.factory.MergerTokenFactory;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+
+/**
+ * An {@link MergerToken} to use to set type, length and precision.
+ */
+public class SetColumnTypeToDb extends AbstractToDbToken.Entity {
+
+    private DbAttribute columnOriginal;
+    private DbAttribute columnNew;
+
+    public SetColumnTypeToDb(DbEntity entity, DbAttribute columnOriginal, DbAttribute columnNew) {
+        super("Set Column Type", entity);
+        this.columnOriginal = columnOriginal;
+        this.columnNew = columnNew;
+    }
+    
+    /**
+     * append the part of the token before the actual column data type
+     * @param context 
+     */
+    protected void appendPrefix(StringBuffer sqlBuffer, QuotingStrategy context) {
+        sqlBuffer.append("ALTER TABLE ");
+        sqlBuffer.append(context.quotedFullyQualifiedName(getEntity()));
+        sqlBuffer.append(" ALTER ");
+        sqlBuffer.append(context.quotedName(columnNew));
+        sqlBuffer.append(" TYPE ");
+    }
+
+    @Override
+    public List<String> createSql(DbAdapter adapter) {
+        StringBuffer sqlBuffer = new StringBuffer();
+        appendPrefix(sqlBuffer, adapter.getQuotingStrategy());
+  
+        sqlBuffer.append(JdbcAdapter.getType(adapter, columnNew));
+        sqlBuffer.append(JdbcAdapter.sizeAndPrecision(adapter, columnNew));
+
+        return Collections.singletonList(sqlBuffer.toString());
+    }
+
+    @Override
+    public String getTokenValue() {
+        StringBuilder sb = new StringBuilder();
+        sb.append(getEntity().getName());
+        sb.append(".");
+        sb.append(columnNew.getName());
+
+        if (columnOriginal.getType() != columnNew.getType()) {
+            sb.append(" type: ");
+            sb.append(TypesMapping.getSqlNameByType(columnOriginal.getType()));
+            sb.append(" -> ");
+            sb.append(TypesMapping.getSqlNameByType(columnNew.getType()));
+        }
+
+        if (columnOriginal.getMaxLength() != columnNew.getMaxLength()) {
+            sb.append(" maxLength: ");
+            sb.append(columnOriginal.getMaxLength());
+            sb.append(" -> ");
+            sb.append(columnNew.getMaxLength());
+        }
+
+        if (columnOriginal.getAttributePrecision() != columnNew.getAttributePrecision()) {
+            sb.append(" precision: ");
+            sb.append(columnOriginal.getAttributePrecision());
+            sb.append(" -> ");
+            sb.append(columnNew.getAttributePrecision());
+        }
+
+        if (columnOriginal.getScale() != columnNew.getScale()) {
+            sb.append(" scale: ");
+            sb.append(columnOriginal.getScale());
+            sb.append(" -> ");
+            sb.append(columnNew.getScale());
+        }
+
+        return sb.toString();
+    }
+
+    public MergerToken createReverse(MergerTokenFactory factory) {
+        return factory.createSetColumnTypeToModel(getEntity(), columnNew, columnOriginal);
+    }
+
+    public DbAttribute getColumnOriginal() {
+        return columnOriginal;
+    }
+
+    public DbAttribute getColumnNew() {
+        return columnNew;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/SetColumnTypeToModel.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/SetColumnTypeToModel.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/SetColumnTypeToModel.java
new file mode 100644
index 0000000..262c3a6
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/SetColumnTypeToModel.java
@@ -0,0 +1,102 @@
+/*****************************************************************
+ *   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.TypesMapping;
+import org.apache.cayenne.dbsync.merge.factory.MergerTokenFactory;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+
+/**
+ * A {@link MergerToken} that modifies one original {@link DbAttribute} to match another
+ * new {@link DbAttribute}s type, maxLength and precision. The name and mandatory fields
+ * are not modified by this token.
+ * 
+ */
+public class SetColumnTypeToModel extends AbstractToModelToken.Entity {
+
+    private DbAttribute columnOriginal;
+    private DbAttribute columnNew;
+
+    public SetColumnTypeToModel(DbEntity entity, DbAttribute columnOriginal,
+            DbAttribute columnNew) {
+        super("Set Column Type", entity);
+        this.columnOriginal = columnOriginal;
+        this.columnNew = columnNew;
+    }
+
+    public MergerToken createReverse(MergerTokenFactory factory) {
+        return factory.createSetColumnTypeToDb(getEntity(), columnNew, columnOriginal);
+    }
+
+    public void execute(MergerContext mergerContext) {
+        columnOriginal.setType(columnNew.getType());
+        columnOriginal.setMaxLength(columnNew.getMaxLength());
+        columnOriginal.setAttributePrecision(columnNew.getAttributePrecision());
+        columnOriginal.setScale(columnNew.getScale());
+        mergerContext.getModelMergeDelegate().dbAttributeModified(columnOriginal);
+    }
+
+    @Override
+    public String getTokenValue() {
+        StringBuilder sb = new StringBuilder();
+        sb.append(getEntity().getName());
+        sb.append(".");
+        sb.append(columnNew.getName());
+
+        if (columnOriginal.getType() != columnNew.getType()) {
+            sb.append(" type: ");
+            sb.append(TypesMapping.getSqlNameByType(columnOriginal.getType()));
+            sb.append(" -> ");
+            sb.append(TypesMapping.getSqlNameByType(columnNew.getType()));
+        }
+
+        if (columnOriginal.getMaxLength() != columnNew.getMaxLength()) {
+            sb.append(" maxLength: ");
+            sb.append(columnOriginal.getMaxLength());
+            sb.append(" -> ");
+            sb.append(columnNew.getMaxLength());
+        }
+
+        if (columnOriginal.getAttributePrecision() != columnNew.getAttributePrecision()) {
+            sb.append(" precision: ");
+            sb.append(columnOriginal.getAttributePrecision());
+            sb.append(" -> ");
+            sb.append(columnNew.getAttributePrecision());
+        }
+
+        if (columnOriginal.getScale() != columnNew.getScale()) {
+            sb.append(" scale: ");
+            sb.append(columnOriginal.getScale());
+            sb.append(" -> ");
+            sb.append(columnNew.getScale());
+        }
+
+        return sb.toString();
+    }
+    
+    public DbAttribute getColumnOriginal() {
+        return columnOriginal;
+    }
+
+    public DbAttribute getColumnNew() {
+        return columnNew;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/SetNotNullToDb.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/SetNotNullToDb.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/SetNotNullToDb.java
new file mode 100644
index 0000000..169ea34
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/SetNotNullToDb.java
@@ -0,0 +1,51 @@
+/*****************************************************************
+ *   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;
+
+/**
+ * A {@link MergerToken} to add a "not null" clause to a column.
+ */
+public class SetNotNullToDb extends AbstractToDbToken.EntityAndColumn {
+
+    public SetNotNullToDb(DbEntity entity, DbAttribute column) {
+        super("Set Not Null", entity, column);
+    }
+
+    @Override
+    public List<String> createSql(DbAdapter adapter) {
+        QuotingStrategy context = adapter.getQuotingStrategy();
+
+        return Collections.singletonList("ALTER TABLE " + context.quotedFullyQualifiedName(getEntity())
+                + " ALTER COLUMN " + context.quotedName(getColumn()) + " SET NOT NULL");
+    }
+
+    public MergerToken createReverse(MergerTokenFactory factory) {
+        return factory.createSetAllowNullToModel(getEntity(), getColumn());
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/SetNotNullToModel.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/SetNotNullToModel.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/SetNotNullToModel.java
new file mode 100644
index 0000000..9ae95b5
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/SetNotNullToModel.java
@@ -0,0 +1,43 @@
+/*****************************************************************
+ *   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;
+
+/**
+ * A {@link MergerToken} to set the mandatory field of a {@link DbAttribute} to true
+ * 
+ */
+public class SetNotNullToModel extends AbstractToModelToken.EntityAndColumn {
+
+    public SetNotNullToModel(DbEntity entity, DbAttribute column) {
+        super("Set Not Null", entity, column);
+    }
+    
+    public MergerToken createReverse(MergerTokenFactory factory) {
+        return factory.createSetAllowNullToDb(getEntity(), getColumn());
+    }
+
+    public void execute(MergerContext mergerContext) {
+        getColumn().setMandatory(true);
+        mergerContext.getModelMergeDelegate().dbAttributeModified(getColumn());
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/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
new file mode 100644
index 0000000..8161b87
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/SetPrimaryKeyToDb.java
@@ -0,0 +1,86 @@
+/*****************************************************************
+ *   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.Iterator;
+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 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());
+    }
+
+    public MergerToken createReverse(MergerTokenFactory factory) {
+        return factory.createSetPrimaryKeyToModel(getEntity(), primaryKeyNew, primaryKeyOriginal,
+                detectedPrimaryKeyName);
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/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
new file mode 100644
index 0000000..6785a2f
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/SetPrimaryKeyToModel.java
@@ -0,0 +1,78 @@
+/*****************************************************************
+ *   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.HashSet;
+import java.util.Set;
+
+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;
+
+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());
+        }
+    }
+
+    public MergerToken createReverse(MergerTokenFactory factory) {
+        return factory.createSetPrimaryKeyToDb(
+                getEntity(),
+                primaryKeyNew,
+                primaryKeyOriginal,
+                detectedPrimaryKeyName);
+    }
+
+    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.getModelMergeDelegate().dbAttributeModified(attr);
+            }
+
+        }
+
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/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
new file mode 100644
index 0000000..1ffdd49
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/SetValueForNullToDb.java
@@ -0,0 +1,47 @@
+/*****************************************************************
+ *   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.List;
+
+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;
+
+
+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());
+    }
+
+    public MergerToken createReverse(MergerTokenFactory factory) {
+        return new DummyReverseToken(this);
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/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
new file mode 100644
index 0000000..a07efff
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/ValueForNullProvider.java
@@ -0,0 +1,42 @@
+/*****************************************************************
+ *   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.List;
+
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+
+/**
+ * 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
+     */
+    public boolean hasValueFor(DbEntity entity, DbAttribute column);
+
+    /**
+     * @return a {@link List} of sql to set value for null
+     */
+    public List<String> createSql(DbEntity entity, DbAttribute column);
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/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
new file mode 100644
index 0000000..586a5cd
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/factory/DB2MergerTokenFactory.java
@@ -0,0 +1,47 @@
+/*****************************************************************
+ *   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.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.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+
+public class DB2MergerTokenFactory extends DefaultMergerTokenFactory {
+
+    @Override
+    public MergerToken createSetColumnTypeToDb(
+            final DbEntity entity,
+            DbAttribute columnOriginal,
+            final DbAttribute columnNew) {
+
+        return new SetColumnTypeToDb(entity, columnOriginal, columnNew) {
+
+            @Override
+            protected void appendPrefix(StringBuffer sqlBuffer, QuotingStrategy context) {
+                sqlBuffer.append("ALTER TABLE ");
+                sqlBuffer.append(context.quotedFullyQualifiedName(entity));
+                sqlBuffer.append(" ALTER COLUMN ");
+                sqlBuffer.append(context.quotedName(columnNew));
+                sqlBuffer.append(" SET DATA TYPE ");
+            }
+        };
+    }
+}