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:38:56 UTC

[10/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/test/java/org/apache/cayenne/dbsync/merge/MergeCase.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/MergeCase.java b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/MergeCase.java
new file mode 100644
index 0000000..c2357b0
--- /dev/null
+++ b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/MergeCase.java
@@ -0,0 +1,209 @@
+/*****************************************************************
+ *   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.configuration.server.ServerRuntime;
+import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.dbsync.merge.factory.MergerTokenFactory;
+import org.apache.cayenne.dbsync.merge.factory.MergerTokenFactoryProvider;
+import org.apache.cayenne.dbsync.reverse.DbLoaderConfiguration;
+import org.apache.cayenne.dbsync.reverse.filters.FiltersConfig;
+import org.apache.cayenne.dbsync.reverse.filters.PatternFilter;
+import org.apache.cayenne.dbsync.reverse.filters.TableFilter;
+import org.apache.cayenne.dbsync.unit.DbSyncCase;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.map.DataMap;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.test.jdbc.DBHelper;
+import org.apache.cayenne.unit.UnitDbAdapter;
+import org.apache.cayenne.unit.di.server.CayenneProjects;
+import org.apache.cayenne.unit.di.server.ServerCaseDataSourceFactory;
+import org.apache.cayenne.unit.di.server.UseServerRuntime;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.junit.Before;
+
+import java.sql.Connection;
+import java.sql.Statement;
+import java.sql.Types;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+@UseServerRuntime(CayenneProjects.TESTMAP_PROJECT)
+public abstract class MergeCase extends DbSyncCase {
+
+    @Inject
+    protected EntityResolver resolver;
+    @Inject
+    protected DataNode node;
+    protected DataMap map;
+    private Log logger = LogFactory.getLog(MergeCase.class);
+    @Inject
+    private DBHelper dbHelper;
+    @Inject
+    private ServerRuntime runtime;
+    @Inject
+    private UnitDbAdapter accessStackAdapter;
+    @Inject
+    private ServerCaseDataSourceFactory dataSourceFactory;
+
+    @Override
+    public void cleanUpDB() throws Exception {
+        dbHelper.update("ARTGROUP").set("PARENT_GROUP_ID", null, Types.INTEGER).execute();
+        super.cleanUpDB();
+    }
+
+    @Before
+    public void setUp() throws Exception {
+
+        // this map can't be safely modified in this test, as it is reset by DI
+        // container
+        // on every test
+        map = runtime.getDataDomain().getDataMap("testmap");
+
+        filterDataMap();
+
+        List<MergerToken> tokens = createMergeTokens();
+        execute(tokens);
+
+        assertTokensAndExecute(0, 0);
+    }
+
+    protected DbMerger createMerger() {
+        return createMerger(null);
+    }
+
+    protected DbMerger createMerger(ValueForNullProvider valueForNullProvider) {
+        return new DbMerger(mergerFactory(), valueForNullProvider);
+    }
+
+    protected List<MergerToken> createMergeTokens() {
+        DbLoaderConfiguration loaderConfiguration = new DbLoaderConfiguration();
+        loaderConfiguration.setFiltersConfig(FiltersConfig.create(null, null,
+                TableFilter.include("ARTIST|GALLERY|PAINTING|NEW_TABLE2?"), PatternFilter.INCLUDE_NOTHING));
+
+        return createMerger().createMergeTokens(node.getDataSource(), node.getAdapter(), map, loaderConfiguration);
+    }
+
+    /**
+     * Remote binary pk {@link DbEntity} for {@link DbAdapter} not supporting
+     * that and so on.
+     */
+    private void filterDataMap() {
+        // copied from AbstractAccessStack.dbEntitiesInInsertOrder
+        boolean excludeBinPK = accessStackAdapter.supportsBinaryPK();
+
+        if (!excludeBinPK) {
+            return;
+        }
+
+        List<DbEntity> entitiesToRemove = new ArrayList<DbEntity>();
+
+        for (DbEntity ent : map.getDbEntities()) {
+            for (DbAttribute attr : ent.getAttributes()) {
+                // check for BIN PK or FK to BIN Pk
+                if (attr.getType() == Types.BINARY || attr.getType() == Types.VARBINARY
+                        || attr.getType() == Types.LONGVARBINARY) {
+
+                    if (attr.isPrimaryKey() || attr.isForeignKey()) {
+                        entitiesToRemove.add(ent);
+                        break;
+                    }
+                }
+            }
+        }
+
+        for (DbEntity e : entitiesToRemove) {
+            map.removeDbEntity(e.getName(), true);
+        }
+    }
+
+    protected void execute(List<MergerToken> tokens) {
+        MergerContext mergerContext = MergerContext.builder(map).dataNode(node).build();
+        for (MergerToken tok : tokens) {
+            tok.execute(mergerContext);
+        }
+    }
+
+    protected void execute(MergerToken token) throws Exception {
+        MergerContext mergerContext = MergerContext.builder(map).dataNode(node).build();
+        token.execute(mergerContext);
+    }
+
+    private void executeSql(String sql) throws Exception {
+
+        try (Connection conn = dataSourceFactory.getSharedDataSource().getConnection();) {
+
+            try (Statement st = conn.createStatement();) {
+                st.execute(sql);
+            }
+        }
+    }
+
+    protected void assertTokens(List<MergerToken> tokens, int expectedToDb, int expectedToModel) {
+        int actualToDb = 0;
+        int actualToModel = 0;
+        for (MergerToken token : tokens) {
+            if (token.getDirection().isToDb()) {
+                actualToDb++;
+            } else if (token.getDirection().isToModel()) {
+                actualToModel++;
+            }
+        }
+
+        assertEquals("tokens to db", expectedToDb, actualToDb);
+        assertEquals("tokens to model", expectedToModel, actualToModel);
+    }
+
+    protected void assertTokensAndExecute(int expectedToDb, int expectedToModel) {
+        List<MergerToken> tokens = createMergeTokens();
+        assertTokens(tokens, expectedToDb, expectedToModel);
+        execute(tokens);
+    }
+
+    protected MergerTokenFactory mergerFactory() {
+        return runtime.getInjector().getInstance(MergerTokenFactoryProvider.class).get(node.getAdapter());
+    }
+
+    protected void dropTableIfPresent(String tableName) throws Exception {
+
+        // must have a dummy datamap for the dummy table for the downstream code
+        // to work
+        DataMap map = new DataMap("dummy");
+        map.setQuotingSQLIdentifiers(map.isQuotingSQLIdentifiers());
+        DbEntity entity = new DbEntity(tableName);
+        map.addDbEntity(entity);
+
+        AbstractToDbToken t = (AbstractToDbToken) mergerFactory().createDropTableToDb(entity);
+
+        for (String sql : t.createSql(node.getAdapter())) {
+
+            try {
+                executeSql(sql);
+            } catch (Exception e) {
+                logger.info("Exception dropping table " + tableName + ", probably abscent..");
+            }
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/MergerFactoryIT.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/MergerFactoryIT.java b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/MergerFactoryIT.java
new file mode 100644
index 0000000..1fe7da7
--- /dev/null
+++ b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/MergerFactoryIT.java
@@ -0,0 +1,310 @@
+/*****************************************************************
+ *   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 static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import java.sql.Types;
+
+import org.apache.cayenne.CayenneDataObject;
+import org.apache.cayenne.access.DataContext;
+import org.apache.cayenne.di.Inject;
+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;
+import org.junit.Test;
+
+public class MergerFactoryIT extends MergeCase {
+
+    @Inject
+    private DataContext context;
+
+    @Test
+    public void testAddAndDropColumnToDb() throws Exception {
+        DbEntity dbEntity = map.getDbEntity("PAINTING");
+        assertNotNull(dbEntity);
+
+        // create and add new column to model and db
+        DbAttribute column = new DbAttribute("NEWCOL1", Types.VARCHAR, dbEntity);
+
+        column.setMandatory(false);
+        column.setMaxLength(10);
+        dbEntity.addAttribute(column);
+        assertTokensAndExecute(1, 0);
+
+        // try merge once more to check that is was merged
+        assertTokensAndExecute(0, 0);
+
+        // remove it from model and db
+        dbEntity.removeAttribute(column.getName());
+        assertTokensAndExecute(1, 0);
+        assertTokensAndExecute(0, 0);
+    }
+
+    @Test
+    public void testChangeVarcharSizeToDb() throws Exception {
+        DbEntity dbEntity = map.getDbEntity("PAINTING");
+        assertNotNull(dbEntity);
+
+        // create and add new column to model and db
+        DbAttribute column = new DbAttribute("NEWCOL2", Types.VARCHAR, dbEntity);
+
+        column.setMandatory(false);
+        column.setMaxLength(10);
+        dbEntity.addAttribute(column);
+        assertTokensAndExecute(1, 0);
+
+        // check that is was merged
+        assertTokensAndExecute(0, 0);
+
+        // change size
+        column.setMaxLength(20);
+
+        // merge to db
+        assertTokensAndExecute(1, 0);
+
+        // check that is was merged
+        assertTokensAndExecute(0, 0);
+
+        // clean up
+        dbEntity.removeAttribute(column.getName());
+        assertTokensAndExecute(1, 0);
+        assertTokensAndExecute(0, 0);
+    }
+
+    @Test
+    public void testMultipleTokensToDb() throws Exception {
+        DbEntity dbEntity = map.getDbEntity("PAINTING");
+        assertNotNull(dbEntity);
+
+        DbAttribute column1 = new DbAttribute("NEWCOL3", Types.VARCHAR, dbEntity);
+        column1.setMandatory(false);
+        column1.setMaxLength(10);
+        dbEntity.addAttribute(column1);
+        DbAttribute column2 = new DbAttribute("NEWCOL4", Types.VARCHAR, dbEntity);
+        column2.setMandatory(false);
+        column2.setMaxLength(10);
+        dbEntity.addAttribute(column2);
+
+        assertTokensAndExecute(2, 0);
+
+        // check that is was merged
+        assertTokensAndExecute(0, 0);
+
+        // change size
+        column1.setMaxLength(20);
+        column2.setMaxLength(30);
+
+        // merge to db
+        assertTokensAndExecute(2, 0);
+
+        // check that is was merged
+        assertTokensAndExecute(0, 0);
+
+        // clean up
+        dbEntity.removeAttribute(column1.getName());
+        dbEntity.removeAttribute(column2.getName());
+        assertTokensAndExecute(2, 0);
+        assertTokensAndExecute(0, 0);
+    }
+
+    @Test
+    public void testAddTableToDb() throws Exception {
+        dropTableIfPresent("NEW_TABLE");
+
+        assertTokensAndExecute(0, 0);
+
+        DbEntity dbEntity = new DbEntity("NEW_TABLE");
+
+        DbAttribute column1 = new DbAttribute("ID", Types.INTEGER, dbEntity);
+        column1.setMandatory(true);
+        column1.setPrimaryKey(true);
+        dbEntity.addAttribute(column1);
+
+        DbAttribute column2 = new DbAttribute("NAME", Types.VARCHAR, dbEntity);
+        column2.setMaxLength(10);
+        column2.setMandatory(false);
+        dbEntity.addAttribute(column2);
+
+        map.addDbEntity(dbEntity);
+
+        assertTokensAndExecute(1, 0);
+        assertTokensAndExecute(0, 0);
+
+        ObjEntity objEntity = new ObjEntity("NewTable");
+        objEntity.setDbEntity(dbEntity);
+        ObjAttribute oatr1 = new ObjAttribute("name");
+        oatr1.setDbAttributePath(column2.getName());
+        oatr1.setType("java.lang.String");
+        objEntity.addAttribute(oatr1);
+        map.addObjEntity(objEntity);
+
+        for (int i = 0; i < 5; i++) {
+            CayenneDataObject dao = (CayenneDataObject) context.newObject(objEntity
+                    .getName());
+            dao.writeProperty(oatr1.getName(), "test " + i);
+        }
+        context.commitChanges();
+
+        // clear up
+        map.removeObjEntity(objEntity.getName(), true);
+        map.removeDbEntity(dbEntity.getName(), true);
+        resolver.refreshMappingCache();
+        assertNull(map.getObjEntity(objEntity.getName()));
+        assertNull(map.getDbEntity(dbEntity.getName()));
+        assertFalse(map.getDbEntities().contains(dbEntity));
+
+        assertTokensAndExecute(1, 0);
+        assertTokensAndExecute(0, 0);
+    }
+
+    @Test
+    public void testAddForeignKeyWithTable() throws Exception {
+        dropTableIfPresent("NEW_TABLE");
+
+        assertTokensAndExecute(0, 0);
+
+        DbEntity dbEntity = new DbEntity("NEW_TABLE");
+
+        attr(dbEntity, "ID", Types.INTEGER, true, true);
+        attr(dbEntity, "NAME", Types.VARCHAR, false, false).setMaxLength(10);
+        attr(dbEntity, "ARTIST_ID", Types.BIGINT, false, false);
+
+        map.addDbEntity(dbEntity);
+
+        DbEntity artistDbEntity = map.getDbEntity("ARTIST");
+        assertNotNull(artistDbEntity);
+
+        // relation from new_table to artist
+        DbRelationship r1 = new DbRelationship("toArtistR1");
+        r1.setSourceEntity(dbEntity);
+        r1.setTargetEntityName(artistDbEntity);
+        r1.setToMany(false);
+        r1.addJoin(new DbJoin(r1, "ARTIST_ID", "ARTIST_ID"));
+        dbEntity.addRelationship(r1);
+
+        // relation from artist to new_table
+        DbRelationship r2 = new DbRelationship("toNewTableR2");
+        r2.setSourceEntity(artistDbEntity);
+        r2.setTargetEntityName(dbEntity);
+        r2.setToMany(true);
+        r2.addJoin(new DbJoin(r2, "ARTIST_ID", "ARTIST_ID"));
+        artistDbEntity.addRelationship(r2);
+
+        assertTokensAndExecute(2, 0);
+        assertTokensAndExecute(0, 0);
+
+        // remove relationships
+        dbEntity.removeRelationship(r1.getName());
+        artistDbEntity.removeRelationship(r2.getName());
+        resolver.refreshMappingCache();
+        /*
+         * Db -Rel 'toArtistR1' - NEW_TABLE 1 -> 1 ARTIST"
+r2 =     * Db -Rel 'toNewTableR2' - ARTIST 1 -> * NEW_TABLE"
+         * */
+        assertTokensAndExecute(1, 1);
+        assertTokensAndExecute(0, 0);
+
+        // clear up
+        // map.removeObjEntity(objEntity.getName(), true);
+        map.removeDbEntity(dbEntity.getName(), true);
+        resolver.refreshMappingCache();
+        // assertNull(map.getObjEntity(objEntity.getName()));
+        assertNull(map.getDbEntity(dbEntity.getName()));
+        assertFalse(map.getDbEntities().contains(dbEntity));
+
+        assertTokensAndExecute(1, 0);
+        assertTokensAndExecute(0, 0);
+    }
+
+    @Test
+    public void testAddForeignKeyAfterTable() throws Exception {
+        dropTableIfPresent("NEW_TABLE");
+
+        assertTokensAndExecute(0, 0);
+
+        DbEntity dbEntity = new DbEntity("NEW_TABLE");
+        attr(dbEntity, "ID", Types.INTEGER, true, true);
+        attr(dbEntity, "NAME", Types.VARCHAR, false, false).setMaxLength(10);
+        attr(dbEntity, "ARTIST_ID", Types.BIGINT, false, false);
+
+        map.addDbEntity(dbEntity);
+
+        DbEntity artistDbEntity = map.getDbEntity("ARTIST");
+        assertNotNull(artistDbEntity);
+
+        assertTokensAndExecute(1, 0);
+        assertTokensAndExecute(0, 0);
+
+        // relation from new_table to artist
+        DbRelationship r1 = new DbRelationship("toArtistR1");
+        r1.setSourceEntity(dbEntity);
+        r1.setTargetEntityName(artistDbEntity);
+        r1.setToMany(false);
+        r1.addJoin(new DbJoin(r1, "ARTIST_ID", "ARTIST_ID"));
+        dbEntity.addRelationship(r1);
+
+        // relation from artist to new_table
+        DbRelationship r2 = new DbRelationship("toNewTableR2");
+        r2.setSourceEntity(artistDbEntity);
+        r2.setTargetEntityName(dbEntity);
+        r2.setToMany(true);
+        r2.addJoin(new DbJoin(r2, "ARTIST_ID", "ARTIST_ID"));
+        artistDbEntity.addRelationship(r2);
+
+        assertTokensAndExecute(1, 0);
+        assertTokensAndExecute(0, 0);
+
+        // remove relationships
+        dbEntity.removeRelationship(r1.getName());
+        artistDbEntity.removeRelationship(r2.getName());
+        resolver.refreshMappingCache();
+        /*
+        * Add Relationship ARTIST->NEW_TABLE To Model
+        * Drop Relationship NEW_TABLE->ARTIST To DB
+        * */
+        assertTokensAndExecute(1, 1);
+        assertTokensAndExecute(0, 0);
+
+        // clear up
+        // map.removeObjEntity(objEntity.getName(), true);
+        map.removeDbEntity(dbEntity.getName(), true);
+        resolver.refreshMappingCache();
+        // assertNull(map.getObjEntity(objEntity.getName()));
+        assertNull(map.getDbEntity(dbEntity.getName()));
+        assertFalse(map.getDbEntities().contains(dbEntity));
+
+        assertTokensAndExecute(1, 0);
+        assertTokensAndExecute(0, 0);
+    }
+
+    private static DbAttribute attr(DbEntity dbEntity, String name, int type, boolean mandatory, boolean primaryKey) {
+        DbAttribute column1 = new DbAttribute(name, type, dbEntity);
+        column1.setMandatory(mandatory);
+        column1.setPrimaryKey(primaryKey);
+
+        dbEntity.addAttribute(column1);
+        return column1;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/SetAllowNullToDbIT.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/SetAllowNullToDbIT.java b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/SetAllowNullToDbIT.java
new file mode 100644
index 0000000..6391ad0
--- /dev/null
+++ b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/SetAllowNullToDbIT.java
@@ -0,0 +1,66 @@
+/*****************************************************************
+ *   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 static org.junit.Assert.assertNotNull;
+
+import java.sql.Types;
+
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+import org.junit.Test;
+
+public class SetAllowNullToDbIT extends MergeCase {
+
+	@Test
+	public void test() throws Exception {
+		DbEntity dbEntity = map.getDbEntity("PAINTING");
+		assertNotNull(dbEntity);
+
+		// create and add new column to model and db
+		DbAttribute column = new DbAttribute("NEWCOL2", Types.VARCHAR, dbEntity);
+
+		try {
+
+			column.setMandatory(true);
+			column.setMaxLength(10);
+			dbEntity.addAttribute(column);
+			assertTokensAndExecute(2, 0);
+
+			// check that is was merged
+			assertTokensAndExecute(0, 0);
+
+			// set null
+			column.setMandatory(false);
+
+			// merge to db
+			assertTokensAndExecute(1, 0);
+
+			// check that is was merged
+			assertTokensAndExecute(0, 0);
+
+			// clean up
+		} finally {
+			dbEntity.removeAttribute(column.getName());
+			assertTokensAndExecute(1, 0);
+			assertTokensAndExecute(0, 0);
+		}
+	}
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/SetNotNullToDbIT.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/SetNotNullToDbIT.java b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/SetNotNullToDbIT.java
new file mode 100644
index 0000000..508d0f8
--- /dev/null
+++ b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/SetNotNullToDbIT.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 static org.junit.Assert.assertNotNull;
+
+import java.sql.Types;
+
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+import org.junit.Test;
+
+public class SetNotNullToDbIT extends MergeCase {
+
+	@Test
+	public void test() throws Exception {
+		DbEntity dbEntity = map.getDbEntity("PAINTING");
+		assertNotNull(dbEntity);
+
+		// create and add new column to model and db
+		DbAttribute column = new DbAttribute("NEWCOL2", Types.VARCHAR, dbEntity);
+
+		column.setMandatory(false);
+		column.setMaxLength(10);
+		dbEntity.addAttribute(column);
+		assertTokensAndExecute(1, 0);
+
+		// check that is was merged
+		assertTokensAndExecute(0, 0);
+
+		// set not null
+		column.setMandatory(true);
+
+		// merge to db
+		assertTokensAndExecute(1, 0);
+
+		// check that is was merged
+		assertTokensAndExecute(0, 0);
+
+		// clean up
+		dbEntity.removeAttribute(column.getName());
+		assertTokensAndExecute(1, 0);
+		assertTokensAndExecute(0, 0);
+	}
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/SetPrimaryKeyToDbIT.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/SetPrimaryKeyToDbIT.java b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/SetPrimaryKeyToDbIT.java
new file mode 100644
index 0000000..3b513e7
--- /dev/null
+++ b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/SetPrimaryKeyToDbIT.java
@@ -0,0 +1,58 @@
+/*****************************************************************
+ *   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.sql.Types;
+
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+import org.junit.Test;
+
+public class SetPrimaryKeyToDbIT extends MergeCase {
+
+	@Test
+	public void test() throws Exception {
+		dropTableIfPresent("NEW_TABLE");
+		assertTokensAndExecute(0, 0);
+
+		DbEntity dbEntity1 = new DbEntity("NEW_TABLE");
+
+		DbAttribute e1col1 = new DbAttribute("ID1", Types.INTEGER, dbEntity1);
+		e1col1.setMandatory(true);
+		e1col1.setPrimaryKey(true);
+		dbEntity1.addAttribute(e1col1);
+		map.addDbEntity(dbEntity1);
+
+		assertTokensAndExecute(1, 0);
+		assertTokensAndExecute(0, 0);
+
+		DbAttribute e1col2 = new DbAttribute("ID2", Types.INTEGER, dbEntity1);
+		e1col2.setMandatory(true);
+		dbEntity1.addAttribute(e1col2);
+
+		assertTokensAndExecute(2, 0);
+		assertTokensAndExecute(0, 0);
+
+		e1col1.setPrimaryKey(false);
+		e1col2.setPrimaryKey(true);
+
+		assertTokensAndExecute(1, 0);
+		assertTokensAndExecute(0, 0);
+	}
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/TokensReversTest.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/TokensReversTest.java b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/TokensReversTest.java
new file mode 100644
index 0000000..b23128c
--- /dev/null
+++ b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/TokensReversTest.java
@@ -0,0 +1,89 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+package org.apache.cayenne.dbsync.merge;
+
+import org.apache.cayenne.dbsync.merge.factory.HSQLMergerTokenFactory;
+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.junit.Assert;
+import org.junit.Test;
+
+import java.util.Collections;
+
+import static org.apache.cayenne.dbsync.merge.builders.ObjectMother.dbAttr;
+import static org.apache.cayenne.dbsync.merge.builders.ObjectMother.dbEntity;
+
+/**
+ * @since 4.0.
+ */
+public class TokensReversTest {
+
+    @Test
+    public void testReverses() {
+        DbAttribute attr = dbAttr().build();
+        DbEntity entity = dbEntity().attributes(attr).build();
+        DbRelationship rel = new DbRelationship("rel");
+        rel.setSourceEntity(entity);
+        rel.addJoin(new DbJoin(rel, attr.getName(), "dontKnow"));
+
+        test(factory().createAddColumnToDb(entity, attr));
+        test(factory().createAddColumnToModel(entity, attr));
+        test(factory().createDropColumnToDb(entity, attr));
+        test(factory().createDropColumnToModel(entity, attr));
+
+        test(factory().createAddRelationshipToDb(entity, rel));
+        test(factory().createAddRelationshipToModel(entity, rel));
+        test(factory().createDropRelationshipToDb(entity, rel));
+        test(factory().createDropRelationshipToModel(entity, rel));
+
+        test(factory().createCreateTableToDb(entity));
+        test(factory().createCreateTableToModel(entity));
+        test(factory().createDropTableToDb(entity));
+        test(factory().createDropTableToModel(entity));
+
+        test(factory().createSetAllowNullToDb(entity, attr));
+        test(factory().createSetAllowNullToModel(entity, attr));
+        test(factory().createSetNotNullToDb(entity, attr));
+        test(factory().createSetNotNullToModel(entity, attr));
+
+        DbAttribute attr2 = dbAttr().build();
+        test(factory().createSetColumnTypeToDb(entity, attr, attr2));
+        test(factory().createSetColumnTypeToModel(entity, attr, attr2));
+
+        test(factory().createSetPrimaryKeyToDb(entity, Collections.singleton(attr), Collections.singleton(attr2), "PK"));
+        test(factory().createSetPrimaryKeyToModel(entity, Collections.singleton(attr), Collections.singleton(attr2), "PK"));
+
+        test(factory().createSetValueForNullToDb(entity, attr, new DefaultValueForNullProvider()));
+    }
+
+    private void test(MergerToken token1) {
+        MergerToken token2 = token1.createReverse(factory()).createReverse(factory());
+
+        Assert.assertEquals(token1.getTokenName(), token2.getTokenName());
+        Assert.assertEquals(token1.getTokenValue(), token2.getTokenValue());
+        Assert.assertEquals(token1.getDirection(), token2.getDirection());
+    }
+
+    private MergerTokenFactory factory() {
+        return new HSQLMergerTokenFactory();
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/TokensToModelExecutionTest.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/TokensToModelExecutionTest.java b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/TokensToModelExecutionTest.java
new file mode 100644
index 0000000..b9abea1
--- /dev/null
+++ b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/TokensToModelExecutionTest.java
@@ -0,0 +1,80 @@
+/*****************************************************************
+ *   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.dbsync.merge.factory.DefaultMergerTokenFactory;
+import org.apache.cayenne.map.DataMap;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+import org.junit.Test;
+
+import static org.apache.cayenne.dbsync.merge.builders.ObjectMother.dataMap;
+import static org.apache.cayenne.dbsync.merge.builders.ObjectMother.dbAttr;
+import static org.apache.cayenne.dbsync.merge.builders.ObjectMother.dbEntity;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @since 4.0.
+ */
+public class TokensToModelExecutionTest {
+
+    @Test
+    public void testCreateAndDropTable() throws Exception {
+        DbEntity entity = dbEntity().build();
+
+        DataMap dataMap = dataMap().build();
+        assertTrue(dataMap.getDbEntityMap().isEmpty());
+        assertTrue(dataMap.getObjEntityMap().isEmpty());
+
+        MergerContext context = MergerContext.builder(dataMap).dataNode(new DataNode()).build();
+        new DefaultMergerTokenFactory().createCreateTableToModel(entity).execute(context);
+
+        assertEquals(1, dataMap.getDbEntityMap().size());
+        assertEquals(1, dataMap.getObjEntities().size());
+        assertEquals(entity, dataMap.getDbEntity(entity.getName()));
+
+        new DefaultMergerTokenFactory().createDropTableToModel(entity).execute(context);
+        assertTrue(dataMap.getDbEntityMap().isEmpty());
+        assertTrue(dataMap.getObjEntityMap().isEmpty());
+    }
+
+    @Test
+    public void testCreateAndDropColumn() throws Exception {
+        DbAttribute attr = dbAttr("attr").build();
+        DbEntity entity = dbEntity().build();
+
+        DataMap dataMap = dataMap().with(entity).build();
+        assertEquals(1, dataMap.getDbEntityMap().size());
+        assertTrue(dataMap.getObjEntityMap().isEmpty());
+
+        MergerContext context = MergerContext.builder(dataMap).dataNode(new DataNode()).build();
+        new DefaultMergerTokenFactory().createAddColumnToModel(entity, attr).execute(context);
+
+        assertEquals(1, dataMap.getDbEntityMap().size());
+        assertEquals(1, entity.getAttributes().size());
+        assertEquals(attr, entity.getAttribute(attr.getName()));
+
+        new DefaultMergerTokenFactory().createDropColumnToModel(entity, attr).execute(context);
+        assertEquals(1, dataMap.getDbEntityMap().size());
+        assertTrue(entity.getAttributes().isEmpty());
+        assertTrue(dataMap.getObjEntityMap().isEmpty());
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/ValueForNullIT.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/ValueForNullIT.java b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/ValueForNullIT.java
new file mode 100644
index 0000000..e9910fd
--- /dev/null
+++ b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/ValueForNullIT.java
@@ -0,0 +1,127 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+package org.apache.cayenne.dbsync.merge;
+
+import junit.framework.AssertionFailedError;
+import org.apache.cayenne.DataObject;
+import org.apache.cayenne.Persistent;
+import org.apache.cayenne.access.DataContext;
+import org.apache.cayenne.access.jdbc.SQLParameterBinding;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.ExpressionFactory;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.ObjAttribute;
+import org.apache.cayenne.map.ObjEntity;
+import org.apache.cayenne.query.SelectQuery;
+import org.junit.Test;
+
+import java.sql.Types;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+public class ValueForNullIT extends MergeCase {
+
+	private static final String DEFAULT_VALUE_STRING = "DEFSTRING";
+
+	@Inject
+	private DataContext context;
+
+	@Test
+	public void test() throws Exception {
+		DbEntity dbEntity = map.getDbEntity("PAINTING");
+		assertNotNull(dbEntity);
+		ObjEntity objEntity = map.getObjEntity("Painting");
+		assertNotNull(objEntity);
+
+		// insert some rows before adding "not null" column
+		final int nrows = 10;
+		for (int i = 0; i < nrows; i++) {
+			DataObject o = (DataObject) context.newObject("Painting");
+			o.writeProperty("paintingTitle", "ptitle" + i);
+		}
+		context.commitChanges();
+
+		// create and add new column to model and db
+		DbAttribute column = new DbAttribute("NEWCOL2", Types.VARCHAR, dbEntity);
+
+		column.setMandatory(false);
+		column.setMaxLength(10);
+		dbEntity.addAttribute(column);
+		assertTrue(dbEntity.getAttributes().contains(column));
+		assertEquals(column, dbEntity.getAttribute(column.getName()));
+		assertTokensAndExecute(1, 0);
+
+		// need obj attr to be able to query
+		ObjAttribute objAttr = new ObjAttribute("newcol2");
+		objAttr.setDbAttributePath(column.getName());
+		objEntity.addAttribute(objAttr);
+
+		// check that is was merged
+		assertTokensAndExecute(0, 0);
+
+		// set not null
+		column.setMandatory(true);
+
+		// merge to db
+		assertTokensAndExecute(2, 0);
+
+		// check that is was merged
+		assertTokensAndExecute(0, 0);
+
+		// check values for null
+		Expression qual = ExpressionFactory.matchExp(objAttr.getName(), DEFAULT_VALUE_STRING);
+		SelectQuery query = new SelectQuery("Painting", qual);
+		List<Persistent> rows = context.performQuery(query);
+		assertEquals(nrows, rows.size());
+
+		// clean up
+		dbEntity.removeAttribute(column.getName());
+		assertTokensAndExecute(1, 0);
+		assertTokensAndExecute(0, 0);
+	}
+
+	@Override
+	protected DbMerger createMerger(final ValueForNullProvider valueForNullProvider) {
+		return super.createMerger(new DefaultValueForNullProvider() {
+
+			@Override
+			protected SQLParameterBinding get(DbEntity entity, DbAttribute column) {
+				int type = column.getType();
+				switch (type) {
+				case Types.VARCHAR:
+					return new SQLParameterBinding(DEFAULT_VALUE_STRING, type, -1);
+				default:
+					throw new AssertionFailedError("should not get here");
+				}
+			}
+
+			@Override
+			public boolean hasValueFor(DbEntity entity, DbAttribute column) {
+				return true;
+			}
+
+		});
+	}
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/builders/Builder.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/builders/Builder.java b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/builders/Builder.java
new file mode 100644
index 0000000..baf8240
--- /dev/null
+++ b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/builders/Builder.java
@@ -0,0 +1,38 @@
+/*****************************************************************
+ *   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.builders;
+
+/**
+ * Base interface for all domain builders
+ *
+ * @since 4.0.
+ */
+public interface Builder<T> {
+
+    /**
+     * Build valid object. If some required data omitted it will be filled with random data.
+     * */
+    T build();
+
+    /**
+     * Build valid object and add some optional fields randomly.
+     * */
+    T random();
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/builders/DataMapBuilder.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/builders/DataMapBuilder.java b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/builders/DataMapBuilder.java
new file mode 100644
index 0000000..a7aa11c
--- /dev/null
+++ b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/builders/DataMapBuilder.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.builders;
+
+import org.apache.cayenne.map.DataMap;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.map.ObjEntity;
+
+import java.util.Collections;
+
+/**
+ * @since 4.0.
+ */
+public class DataMapBuilder extends DefaultBuilder<DataMap> {
+
+    public DataMapBuilder() {
+        this(new DataMap());
+    }
+
+    public DataMapBuilder(DataMap dataMap) {
+        super(dataMap);
+    }
+
+    public DataMapBuilder with(DbEntity ... entities) {
+        for (DbEntity entity : entities) {
+            obj.addDbEntity(entity);
+        }
+
+        return this;
+    }
+
+    public DataMapBuilder with(DbEntityBuilder ... entities) {
+        for (DbEntityBuilder entity : entities) {
+            obj.addDbEntity(entity.build());
+        }
+
+        return this;
+    }
+
+    public DataMapBuilder withDbEntities(int count) {
+        for (int i = 0; i < count; i++) {
+            obj.addDbEntity(ObjectMother.dbEntity().random());
+        }
+
+        return this;
+    }
+
+    public DataMapBuilder with(ObjEntity... entities) {
+        for (ObjEntity entity : entities) {
+            obj.addObjEntity(entity);
+        }
+
+        return this;
+    }
+
+    public DataMapBuilder with(ObjEntityBuilder ... entities) {
+        for (ObjEntityBuilder entity : entities) {
+            obj.addObjEntity(entity.build());
+        }
+
+        return this;
+    }
+
+    public DataMapBuilder withObjEntities(int count) {
+        for (int i = 0; i < count; i++) {
+            obj.addObjEntity(ObjectMother.objEntity().random());
+        }
+
+        return this;
+    }
+
+    public DataMapBuilder join(String from, String to) {
+        return join(null, from, to);
+    }
+
+    public DataMapBuilder join(String name, String from, String to) {
+        String[] fromSplit = from.split("\\.");
+        DbEntity fromEntity = obj.getDbEntity(fromSplit[0]);
+        if (fromEntity == null) {
+            throw new IllegalArgumentException("Entity '" + fromSplit[0] + "' is undefined");
+        }
+
+        String[] toSplit = to.split("\\.");
+
+        fromEntity.addRelationship(new DbRelationshipBuilder(name)
+                .from(fromEntity, fromSplit[1])
+                .to(toSplit[0], toSplit[1])
+
+                .build());
+
+        return this;
+    }
+
+    public DataMap build() {
+        if (obj.getNamespace() == null) {
+            obj.setNamespace(new EntityResolver(Collections.singleton(obj)));
+        }
+
+        return obj;
+    }
+
+    @Override
+    public DataMap random() {
+        if (dataFactory.chance(90)) {
+            withDbEntities(dataFactory.getNumberUpTo(10));
+        }
+
+
+        return build();
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/builders/DbAttributeBuilder.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/builders/DbAttributeBuilder.java b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/builders/DbAttributeBuilder.java
new file mode 100644
index 0000000..2d600f3
--- /dev/null
+++ b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/builders/DbAttributeBuilder.java
@@ -0,0 +1,115 @@
+/*****************************************************************
+ *   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.builders;
+
+import org.apache.cayenne.datafactory.DictionaryValueProvider;
+import org.apache.cayenne.datafactory.ValueProvider;
+import org.apache.cayenne.dba.TypesMapping;
+import org.apache.cayenne.map.DbAttribute;
+
+import static org.apache.commons.lang.StringUtils.isEmpty;
+
+/**
+ * @since 4.0.
+ */
+public class DbAttributeBuilder extends DefaultBuilder<DbAttribute> {
+
+    private static final ValueProvider<String> TYPES_RANDOM = new DictionaryValueProvider<String>(ValueProvider.RANDOM) {
+        @Override
+        protected String[] values() {
+            return TypesMapping.getDatabaseTypes();
+        }
+    };
+
+    public DbAttributeBuilder() {
+        super(new DbAttribute());
+    }
+
+    public DbAttributeBuilder name() {
+        return name(getRandomJavaName());
+    }
+
+    public DbAttributeBuilder name(String name) {
+        obj.setName(name);
+
+        return this;
+    }
+
+    public DbAttributeBuilder type() {
+        return type(TYPES_RANDOM.randomValue());
+    }
+
+    public DbAttributeBuilder type(String item) {
+        obj.setType(TypesMapping.getSqlTypeByName(item));
+
+        return this;
+    }
+
+    public DbAttributeBuilder typeInt() {
+        return type(TypesMapping.SQL_INTEGER);
+    }
+
+    public DbAttributeBuilder typeBigInt() {
+        return type(TypesMapping.SQL_BIGINT);
+    }
+
+    public DbAttributeBuilder typeVarchar(int length) {
+        type(TypesMapping.SQL_VARCHAR);
+        length(length);
+
+        return this;
+    }
+
+    private DbAttributeBuilder length(int length) {
+        obj.setMaxLength(length);
+
+        return this;
+    }
+
+    public DbAttributeBuilder primaryKey() {
+        obj.setPrimaryKey(true);
+
+        return this;
+    }
+
+    public DbAttributeBuilder mandatory() {
+        obj.setMandatory(true);
+
+        return this;
+    }
+
+    @Override
+    public DbAttribute build() {
+        if (isEmpty(obj.getName())) {
+            name();
+        }
+
+        if (obj.getType() == TypesMapping.NOT_DEFINED) {
+            type();
+        }
+
+        return obj;
+    }
+
+    @Override
+    public DbAttribute random() {
+        return build();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/builders/DbEntityBuilder.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/builders/DbEntityBuilder.java b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/builders/DbEntityBuilder.java
new file mode 100644
index 0000000..03f0738
--- /dev/null
+++ b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/builders/DbEntityBuilder.java
@@ -0,0 +1,90 @@
+/*****************************************************************
+ *   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.builders;
+
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.commons.lang.StringUtils;
+
+/**
+ * @since 4.0.
+ */
+public class DbEntityBuilder extends DefaultBuilder<DbEntity> {
+
+    public DbEntityBuilder() {
+        super(new DbEntity());
+    }
+
+    public DbEntityBuilder name() {
+        return name(getRandomJavaName());
+    }
+
+    public DbEntityBuilder name(String name) {
+        obj.setName(name);
+
+        return this;
+    }
+
+    public DbEntityBuilder attributes(DbAttribute ... attributes) {
+        for (DbAttribute attribute : attributes) {
+            obj.addAttribute(attribute);
+        }
+
+        return this;
+    }
+
+    public DbEntityBuilder attributes(DbAttributeBuilder ... attributes) {
+        for (DbAttributeBuilder attribute : attributes) {
+            obj.addAttribute(attribute.build());
+        }
+
+        return this;
+    }
+
+    public DbEntityBuilder attributes(int numberUpTo) {
+        for (int i = 0; i < numberUpTo; i++) {
+            try {
+                obj.addAttribute(new DbAttributeBuilder().random());
+            } catch (IllegalArgumentException e) {
+                i--; // try again
+            }
+        }
+
+        return this;
+    }
+
+
+    @Override
+    public DbEntity build() {
+        if (obj.getName() == null) {
+            obj.setName(StringUtils.capitalize(getRandomJavaName()));
+        }
+
+        return obj;
+    }
+
+    @Override
+    public DbEntity random() {
+        if (dataFactory.chance(99)) {
+            attributes(dataFactory.getNumberUpTo(20));
+        }
+
+        return build();
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/builders/DbRelationshipBuilder.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/builders/DbRelationshipBuilder.java b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/builders/DbRelationshipBuilder.java
new file mode 100644
index 0000000..ae87549
--- /dev/null
+++ b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/builders/DbRelationshipBuilder.java
@@ -0,0 +1,85 @@
+/*****************************************************************
+ *   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.builders;
+
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.DbJoin;
+import org.apache.cayenne.map.DbRelationship;
+
+/**
+ * @since 4.0.
+ */
+public class DbRelationshipBuilder extends DefaultBuilder<DbRelationship> {
+
+    private String[] from;
+    private String[] to;
+
+    public DbRelationshipBuilder() {
+        super(new DbRelationship());
+    }
+
+    public DbRelationshipBuilder(String name) {
+        super(new DbRelationship(name));
+    }
+
+    public DbRelationshipBuilder(DbRelationship obj) {
+        super(obj);
+    }
+
+    public DbRelationshipBuilder name() {
+        return name(getRandomJavaName());
+    }
+
+    public DbRelationshipBuilder name(String name) {
+        obj.setName(name);
+
+        return this;
+    }
+
+    public DbRelationshipBuilder from(DbEntity entity, String ... columns) {
+        obj.setSourceEntity(entity);
+        this.from = columns;
+
+        return this;
+    }
+
+    public DbRelationshipBuilder to(String entityName, String ... columns) {
+        obj.setTargetEntityName(entityName);
+        this.to = columns;
+
+        return this;
+    }
+
+    @Override
+    public DbRelationship build() {
+        if (obj.getName() == null) {
+            name();
+        }
+
+        if (from.length != to.length) {
+            throw new IllegalStateException("from and to columns name size mismatch");
+        }
+
+        for (int i = 0; i < from.length; i++) {
+            obj.addJoin(new DbJoin(obj, from[i], to[i]));
+        }
+
+        return obj;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/builders/DefaultBuilder.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/builders/DefaultBuilder.java b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/builders/DefaultBuilder.java
new file mode 100644
index 0000000..559347b
--- /dev/null
+++ b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/builders/DefaultBuilder.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.builders;
+
+import org.apache.cayenne.datafactory.DataFactory;
+import org.apache.commons.lang.StringUtils;
+
+/**
+ * @since 4.0.
+ */
+public abstract class DefaultBuilder<T> implements Builder<T> {
+
+    protected final DataFactory dataFactory;
+    protected final T obj;
+
+
+    protected DefaultBuilder(T obj) {
+        this.dataFactory = new DataFactory();
+        this.obj = obj;
+    }
+
+    public String getRandomJavaName() {
+        int count = dataFactory.getNumberBetween(1, 5);
+        StringBuilder res = new StringBuilder();
+        for (int i = 0; i < count; i++) {
+            res.append(StringUtils.capitalize(dataFactory.getRandomWord()));
+        }
+
+        return StringUtils.uncapitalize(res.toString());
+    }
+
+    @Override
+    public T build() {
+        return obj;
+    }
+
+    @Override
+    public T random() {
+        return build();
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/builders/ObjAttributeBuilder.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/builders/ObjAttributeBuilder.java b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/builders/ObjAttributeBuilder.java
new file mode 100644
index 0000000..a183c34
--- /dev/null
+++ b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/builders/ObjAttributeBuilder.java
@@ -0,0 +1,67 @@
+/*****************************************************************
+ *   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.builders;
+
+import org.apache.cayenne.map.ObjAttribute;
+
+/**
+ * @since 4.0.
+ */
+public class ObjAttributeBuilder extends DefaultBuilder<ObjAttribute> {
+
+    public ObjAttributeBuilder() {
+        super(new ObjAttribute());
+    }
+
+    public ObjAttributeBuilder name() {
+        return name(getRandomJavaName());
+    }
+
+    public ObjAttributeBuilder name(String name) {
+        obj.setName(name);
+
+        return this;
+    }
+
+    public ObjAttributeBuilder type(Class type) {
+        obj.setType(type.getCanonicalName());
+
+        return this;
+    }
+
+    public ObjAttributeBuilder dbPath(String path) {
+        obj.setDbAttributePath(path);
+
+        return this;
+    }
+
+    @Override
+    public ObjAttribute build() {
+        if (obj.getName() == null) {
+            name();
+        }
+
+        return obj;
+    }
+
+    @Override
+    public ObjAttribute random() {
+        return build();
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/builders/ObjEntityBuilder.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/builders/ObjEntityBuilder.java b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/builders/ObjEntityBuilder.java
new file mode 100644
index 0000000..f2f701c
--- /dev/null
+++ b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/builders/ObjEntityBuilder.java
@@ -0,0 +1,98 @@
+/*****************************************************************
+ *   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.builders;
+
+import org.apache.cayenne.map.ObjAttribute;
+import org.apache.cayenne.map.ObjEntity;
+import org.apache.commons.lang.StringUtils;
+
+/**
+ * @since 4.0.
+ */
+public class ObjEntityBuilder extends DefaultBuilder<ObjEntity> {
+
+    public ObjEntityBuilder() {
+        super(new ObjEntity());
+    }
+
+    public ObjEntityBuilder name() {
+        return name(getRandomJavaName());
+    }
+
+    public ObjEntityBuilder name(String name) {
+        obj.setName(name);
+
+        return this;
+    }
+
+    public ObjEntityBuilder attributes(ObjAttribute... attributes) {
+        for (ObjAttribute attribute : attributes) {
+            obj.addAttribute(attribute);
+        }
+
+        return this;
+    }
+
+    public ObjEntityBuilder attributes(ObjAttributeBuilder ... attributes) {
+        for (ObjAttributeBuilder attribute : attributes) {
+            obj.addAttribute(attribute.build());
+        }
+
+        return this;
+    }
+
+    public ObjEntityBuilder attributes(int numberUpTo) {
+        for (int i = 0; i < numberUpTo; i++) {
+            obj.addAttribute(new ObjAttributeBuilder().random());
+        }
+
+        return this;
+    }
+
+
+    @Override
+    public ObjEntity build() {
+        if (obj.getName() == null) {
+            obj.setName(StringUtils.capitalize(getRandomJavaName()));
+        }
+
+        return obj;
+    }
+
+    @Override
+    public ObjEntity random() {
+        if (dataFactory.chance(99)) {
+            attributes(dataFactory.getNumberUpTo(20));
+        }
+
+        return build();
+    }
+
+    public ObjEntityBuilder clazz(String s) {
+        obj.setClassName(s);
+
+        return this;
+    }
+
+    public ObjEntityBuilder dbEntity(String table) {
+        obj.setDbEntityName(table);
+
+        return this;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/builders/ObjectMother.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/builders/ObjectMother.java b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/builders/ObjectMother.java
new file mode 100644
index 0000000..0097fe1
--- /dev/null
+++ b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/builders/ObjectMother.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.builders;
+
+import org.apache.cayenne.map.DataMap;
+
+/**
+ * Factory for test data see pattern definition:
+ * http://martinfowler.com/bliki/ObjectMother.html
+ *
+ * @since 4.0.
+ */
+public class ObjectMother {
+
+    public static DataMapBuilder dataMap() {
+        return new DataMapBuilder();
+    }
+
+    public static DataMapBuilder dataMap(DataMap dataMap) {
+        return new DataMapBuilder(dataMap);
+    }
+
+    public static DbEntityBuilder dbEntity() {
+        return new DbEntityBuilder();
+    }
+
+    public static DbEntityBuilder dbEntity(String name) {
+        return new DbEntityBuilder().name(name);
+    }
+
+    public static ObjEntityBuilder objEntity() {
+        return new ObjEntityBuilder();
+    }
+
+    public static ObjEntityBuilder objEntity(String packageName, String className, String table) {
+        return new ObjEntityBuilder()
+                .name(className)
+                .clazz(packageName + "." + className)
+                .dbEntity(table);
+    }
+
+    public static ObjAttributeBuilder objAttr(String name) {
+        return new ObjAttributeBuilder().name(name);
+    }
+
+    public static DbAttributeBuilder dbAttr(String name) {
+        return dbAttr().name(name);
+    }
+
+    public static DbAttributeBuilder dbAttr() {
+        return new DbAttributeBuilder();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/reverse/DbLoaderIT.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/reverse/DbLoaderIT.java b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/reverse/DbLoaderIT.java
new file mode 100644
index 0000000..a902754
--- /dev/null
+++ b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/reverse/DbLoaderIT.java
@@ -0,0 +1,430 @@
+/*****************************************************************
+ *   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.reverse;
+
+import org.apache.cayenne.dbsync.reverse.filters.FiltersConfig;
+import org.apache.cayenne.dbsync.reverse.filters.PatternFilter;
+import org.apache.cayenne.dbsync.reverse.filters.TableFilter;
+import org.apache.cayenne.configuration.server.ServerRuntime;
+import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.dba.TypesMapping;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.map.*;
+import org.apache.cayenne.unit.UnitDbAdapter;
+import org.apache.cayenne.unit.di.server.CayenneProjects;
+import org.apache.cayenne.unit.di.server.ServerCase;
+import org.apache.cayenne.unit.di.server.ServerCaseDataSourceFactory;
+import org.apache.cayenne.unit.di.server.UseServerRuntime;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.sql.Types;
+import java.util.Collection;
+import java.util.List;
+
+import static org.junit.Assert.*;
+
+@UseServerRuntime(CayenneProjects.TESTMAP_PROJECT)
+public class DbLoaderIT extends ServerCase {
+
+    public static final DbLoaderConfiguration CONFIG = new DbLoaderConfiguration();
+    @Inject
+    private ServerRuntime runtime;
+
+    @Inject
+    private DbAdapter adapter;
+
+    @Inject
+    private ServerCaseDataSourceFactory dataSourceFactory;
+
+    @Inject
+    private UnitDbAdapter accessStackAdapter;
+
+    private DbLoader loader;
+
+    @Before
+    public void setUp() throws Exception {
+        loader = new DbLoader(dataSourceFactory.getSharedDataSource().getConnection(), adapter, null);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        loader.getConnection().close();
+    }
+
+    @Test
+    public void testGetTableTypes() throws Exception {
+
+        List<?> tableTypes = loader.getTableTypes();
+
+        assertNotNull(tableTypes);
+
+        String tableLabel = adapter.tableTypeForTable();
+        if (tableLabel != null) {
+            assertTrue("Missing type for table '" + tableLabel + "' - " + tableTypes, tableTypes.contains(tableLabel));
+        }
+
+        String viewLabel = adapter.tableTypeForView();
+        if (viewLabel != null) {
+            assertTrue("Missing type for view '" + viewLabel + "' - " + tableTypes, tableTypes.contains(viewLabel));
+        }
+    }
+
+    @Test
+    public void testGetTables() throws Exception {
+
+        String tableLabel = adapter.tableTypeForTable();
+
+        List<DetectedDbEntity> tables = loader.createTableLoader(null, null, TableFilter.everything())
+                .getDbEntities(TableFilter.everything(), new String[]{tableLabel});
+
+        assertNotNull(tables);
+
+        boolean foundArtist = false;
+
+        for (DetectedDbEntity table : tables) {
+            if ("ARTIST".equalsIgnoreCase(table.getName())) {
+                foundArtist = true;
+                break;
+            }
+        }
+
+        assertTrue("'ARTIST' is missing from the table list: " + tables, foundArtist);
+    }
+
+    @Test
+    public void testGetTablesWithWrongCatalog() throws Exception {
+
+        DbLoaderConfiguration config = new DbLoaderConfiguration();
+        config.setFiltersConfig(
+                FiltersConfig.create("WRONG", null, TableFilter.everything(), PatternFilter.INCLUDE_NOTHING));
+        List<DetectedDbEntity> tables = loader
+                .createTableLoader("WRONG", null, TableFilter.everything())
+                    .getDbEntities(TableFilter.everything(), new String[]{adapter.tableTypeForTable()});
+
+        assertNotNull(tables);
+        assertTrue(tables.isEmpty());
+    }
+
+    @Test
+    public void testGetTablesWithWrongSchema() throws Exception {
+
+        DbLoaderConfiguration config = new DbLoaderConfiguration();
+        config.setFiltersConfig(
+                FiltersConfig.create(null, "WRONG", TableFilter.everything(), PatternFilter.INCLUDE_NOTHING));
+        List<DetectedDbEntity> tables = loader
+                .createTableLoader(null, "WRONG", TableFilter.everything())
+                .getDbEntities(TableFilter.everything(), new String[]{adapter.tableTypeForTable()});
+
+        assertNotNull(tables);
+        assertTrue(tables.isEmpty());
+    }
+
+    @Test
+    public void testLoadWithMeaningfulPK() throws Exception {
+
+        DataMap map = new DataMap();
+        String[] tableLabel = { adapter.tableTypeForTable() };
+
+        loader.setCreatingMeaningfulPK(true);
+
+        List<DbEntity> entities = loader
+                .createTableLoader(null, null, TableFilter.everything())
+                .loadDbEntities(map, CONFIG, tableLabel);
+
+        loader.loadObjEntities(map, CONFIG, entities);
+
+        ObjEntity artist = map.getObjEntity("Artist");
+        assertNotNull(artist);
+
+        ObjAttribute id = artist.getAttribute("artistId");
+        assertNotNull(id);
+    }
+
+    /**
+     * DataMap loading is in one big test method, since breaking it in
+     * individual tests would require multiple reads of metatdata which is
+     * extremely slow on some RDBMS (Sybase).
+     */
+    @Test
+    public void testLoad() throws Exception {
+
+        boolean supportsUnique = runtime.getDataDomain().getDataNodes().iterator().next().getAdapter()
+                .supportsUniqueConstraints();
+        boolean supportsLobs = accessStackAdapter.supportsLobs();
+        boolean supportsFK = accessStackAdapter.supportsFKConstraints();
+
+        DataMap map = new DataMap();
+        map.setDefaultPackage("foo.x");
+
+        String tableLabel = adapter.tableTypeForTable();
+
+        // *** TESTING THIS ***
+        List<DbEntity> entities = loader
+                .createTableLoader(null, null, TableFilter.everything())
+                .loadDbEntities(map, CONFIG, new String[]{adapter.tableTypeForTable()});
+
+
+        assertDbEntities(map);
+
+        if (supportsLobs) {
+            assertLobDbEntities(map);
+        }
+
+        // *** TESTING THIS ***
+        loader.loadDbRelationships(CONFIG, null, null, entities);
+
+        if (supportsFK) {
+            Collection<DbRelationship> rels = getDbEntity(map, "ARTIST").getRelationships();
+            assertNotNull(rels);
+            assertTrue(!rels.isEmpty());
+
+            // test one-to-one
+            rels = getDbEntity(map, "PAINTING").getRelationships();
+            assertNotNull(rels);
+
+            // find relationship to PAINTING_INFO
+            DbRelationship oneToOne = null;
+            for (DbRelationship rel : rels) {
+                if ("PAINTING_INFO".equalsIgnoreCase(rel.getTargetEntityName())) {
+                    oneToOne = rel;
+                    break;
+                }
+            }
+
+            assertNotNull("No relationship to PAINTING_INFO", oneToOne);
+            assertFalse("Relationship to PAINTING_INFO must be to-one", oneToOne.isToMany());
+            assertTrue("Relationship to PAINTING_INFO must be to-one", oneToOne.isToDependentPK());
+
+            // test UNIQUE only if FK is supported...
+            if (supportsUnique) {
+                assertUniqueConstraintsInRelationships(map);
+            }
+        }
+
+        // *** TESTING THIS ***
+        loader.setCreatingMeaningfulPK(false);
+        loader.loadObjEntities(map, CONFIG, entities);
+
+        assertObjEntities(map);
+
+        // now when the map is loaded, test
+        // various things
+        // selectively check how different types were processed
+        if (accessStackAdapter.supportsColumnTypeReengineering()) {
+            checkTypes(map);
+        }
+    }
+
+    private void assertUniqueConstraintsInRelationships(DataMap map) {
+        // unfortunately JDBC metadata doesn't provide info for UNIQUE
+        // constraints....
+        // cant reengineer them...
+
+        // find rel to TO_ONEFK1
+        /*
+         * Iterator it = getDbEntity(map,
+         * "TO_ONEFK2").getRelationships().iterator(); DbRelationship rel =
+         * (DbRelationship) it.next(); assertEquals("TO_ONEFK1",
+         * rel.getTargetEntityName());
+         * assertFalse("UNIQUE constraint was ignored...", rel.isToMany());
+         */
+    }
+
+    private void assertDbEntities(DataMap map) {
+        DbEntity dae = getDbEntity(map, "ARTIST");
+        assertNotNull("Null 'ARTIST' entity, other DbEntities: " + map.getDbEntityMap(), dae);
+        assertEquals("ARTIST", dae.getName().toUpperCase());
+
+        DbAttribute a = getDbAttribute(dae, "ARTIST_ID");
+        assertNotNull(a);
+        assertTrue(a.isPrimaryKey());
+        assertFalse(a.isGenerated());
+
+        if (adapter.supportsGeneratedKeys()) {
+            DbEntity bag = getDbEntity(map, "GENERATED_COLUMN_TEST");
+            DbAttribute id = getDbAttribute(bag, "GENERATED_COLUMN");
+            assertTrue(id.isPrimaryKey());
+            assertTrue(id.isGenerated());
+        }
+    }
+
+    private void assertObjEntities(DataMap map) {
+
+        boolean supportsLobs = accessStackAdapter.supportsLobs();
+        boolean supportsFK = accessStackAdapter.supportsFKConstraints();
+
+        ObjEntity ae = map.getObjEntity("Artist");
+        assertNotNull(ae);
+        assertEquals("Artist", ae.getName());
+
+        // assert primary key is not an attribute
+        assertNull(ae.getAttribute("artistId"));
+
+        if (supportsLobs) {
+            assertLobObjEntities(map);
+        }
+
+        if (supportsFK) {
+            Collection<?> rels1 = ae.getRelationships();
+            assertNotNull(rels1);
+            assertTrue(rels1.size() > 0);
+        }
+
+        assertEquals("foo.x.Artist", ae.getClassName());
+    }
+
+    private void assertLobDbEntities(DataMap map) {
+        DbEntity blobEnt = getDbEntity(map, "BLOB_TEST");
+        assertNotNull(blobEnt);
+        DbAttribute blobAttr = getDbAttribute(blobEnt, "BLOB_COL");
+        assertNotNull(blobAttr);
+        assertTrue(msgForTypeMismatch(Types.BLOB, blobAttr), Types.BLOB == blobAttr.getType()
+                || Types.LONGVARBINARY == blobAttr.getType());
+
+        DbEntity clobEnt = getDbEntity(map, "CLOB_TEST");
+        assertNotNull(clobEnt);
+        DbAttribute clobAttr = getDbAttribute(clobEnt, "CLOB_COL");
+        assertNotNull(clobAttr);
+        assertTrue(msgForTypeMismatch(Types.CLOB, clobAttr), Types.CLOB == clobAttr.getType()
+                || Types.LONGVARCHAR == clobAttr.getType());
+
+/*
+        DbEntity nclobEnt = getDbEntity(map, "NCLOB_TEST");
+        assertNotNull(nclobEnt);
+        DbAttribute nclobAttr = getDbAttribute(nclobEnt, "NCLOB_COL");
+        assertNotNull(nclobAttr);
+        assertTrue(msgForTypeMismatch(Types.NCLOB, nclobAttr), Types.NCLOB == nclobAttr.getType()
+                || Types.LONGVARCHAR == nclobAttr.getType());
+*/
+    }
+
+    private void assertLobObjEntities(DataMap map) {
+        ObjEntity blobEnt = map.getObjEntity("BlobTest");
+        assertNotNull(blobEnt);
+        // BLOBs should be mapped as byte[]
+        ObjAttribute blobAttr = blobEnt.getAttribute("blobCol");
+        assertNotNull("BlobTest.blobCol failed to doLoad", blobAttr);
+        assertEquals("byte[]", blobAttr.getType());
+
+
+        ObjEntity clobEnt = map.getObjEntity("ClobTest");
+        assertNotNull(clobEnt);
+        // CLOBs should be mapped as Strings by default
+        ObjAttribute clobAttr = clobEnt.getAttribute("clobCol");
+        assertNotNull(clobAttr);
+        assertEquals(String.class.getName(), clobAttr.getType());
+
+
+        ObjEntity nclobEnt = map.getObjEntity("NclobTest");
+        assertNotNull(nclobEnt);
+        // CLOBs should be mapped as Strings by default
+        ObjAttribute nclobAttr = nclobEnt.getAttribute("nclobCol");
+        assertNotNull(nclobAttr);
+        assertEquals(String.class.getName(), nclobAttr.getType());
+    }
+
+    private DbEntity getDbEntity(DataMap map, String name) {
+        DbEntity de = map.getDbEntity(name);
+        // sometimes table names get converted to lowercase
+        if (de == null) {
+            de = map.getDbEntity(name.toLowerCase());
+        }
+
+        return de;
+    }
+
+    private DbAttribute getDbAttribute(DbEntity ent, String name) {
+        DbAttribute da = ent.getAttribute(name);
+        // sometimes table names get converted to lowercase
+        if (da == null) {
+            da = ent.getAttribute(name.toLowerCase());
+        }
+
+        return da;
+    }
+
+    private DataMap originalMap() {
+        return runtime.getDataDomain().getDataNodes().iterator().next().getDataMaps().iterator().next();
+    }
+
+    /**
+     * Selectively check how different types were processed.
+     */
+    public void checkTypes(DataMap map) {
+        DbEntity dbe = getDbEntity(map, "PAINTING");
+        DbEntity floatTest = getDbEntity(map, "FLOAT_TEST");
+        DbEntity smallintTest = getDbEntity(map, "SMALLINT_TEST");
+        DbAttribute integerAttr = getDbAttribute(dbe, "PAINTING_ID");
+        DbAttribute decimalAttr = getDbAttribute(dbe, "ESTIMATED_PRICE");
+        DbAttribute varcharAttr = getDbAttribute(dbe, "PAINTING_TITLE");
+        DbAttribute floatAttr = getDbAttribute(floatTest, "FLOAT_COL");
+        DbAttribute smallintAttr = getDbAttribute(smallintTest, "SMALLINT_COL");
+
+        // check decimal
+        assertTrue(msgForTypeMismatch(Types.DECIMAL, decimalAttr), Types.DECIMAL == decimalAttr.getType()
+                || Types.NUMERIC == decimalAttr.getType());
+        assertEquals(2, decimalAttr.getScale());
+
+        // check varchar
+        assertEquals(msgForTypeMismatch(Types.VARCHAR, varcharAttr), Types.VARCHAR, varcharAttr.getType());
+        assertEquals(255, varcharAttr.getMaxLength());
+        // check integer
+        assertEquals(msgForTypeMismatch(Types.INTEGER, integerAttr), Types.INTEGER, integerAttr.getType());
+        // check float
+        assertTrue(msgForTypeMismatch(Types.FLOAT, floatAttr), Types.FLOAT == floatAttr.getType()
+                || Types.DOUBLE == floatAttr.getType() || Types.REAL == floatAttr.getType());
+
+        // check smallint
+        assertTrue(msgForTypeMismatch(Types.SMALLINT, smallintAttr), Types.SMALLINT == smallintAttr.getType()
+                || Types.INTEGER == smallintAttr.getType());
+    }
+
+    public void checkAllDBEntities(DataMap map) {
+
+        for (DbEntity origEnt : originalMap().getDbEntities()) {
+            DbEntity newEnt = map.getDbEntity(origEnt.getName());
+            for (DbAttribute origAttr : origEnt.getAttributes()) {
+                DbAttribute newAttr = newEnt.getAttribute(origAttr.getName());
+                assertNotNull("No matching DbAttribute for '" + origAttr.getName(), newAttr);
+                assertEquals(msgForTypeMismatch(origAttr, newAttr), origAttr.getType(), newAttr.getType());
+                // length and precision doesn't have to be the same
+                // it must be greater or equal
+                assertTrue(origAttr.getMaxLength() <= newAttr.getMaxLength());
+                assertTrue(origAttr.getScale() <= newAttr.getScale());
+            }
+        }
+    }
+
+    private static String msgForTypeMismatch(DbAttribute origAttr, DbAttribute newAttr) {
+        return msgForTypeMismatch(origAttr.getType(), newAttr);
+    }
+
+    private static String msgForTypeMismatch(int origType, DbAttribute newAttr) {
+        String nt = TypesMapping.getSqlNameByType(newAttr.getType());
+        String ot = TypesMapping.getSqlNameByType(origType);
+        return attrMismatch(newAttr.getName(), "expected type: <" + ot + ">, but was <" + nt + ">");
+    }
+
+    private static String attrMismatch(String attrName, String msg) {
+        return "[Error loading attribute '" + attrName + "': " + msg + "]";
+    }
+}