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/30 14:23:49 UTC

[3/9] cayenne git commit: CAY-2115 DbLoader - allow loading DataMap without Obj layer

http://git-wip-us.apache.org/repos/asf/cayenne/blob/cf172fc9/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/db/DbLoader.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/db/DbLoader.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/db/DbLoader.java
new file mode 100644
index 0000000..e43c15a
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/db/DbLoader.java
@@ -0,0 +1,702 @@
+/*****************************************************************
+ *   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.db;
+
+import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.dba.TypesMapping;
+import org.apache.cayenne.dbsync.merge.EntityMergeSupport;
+import org.apache.cayenne.dbsync.reverse.filters.CatalogFilter;
+import org.apache.cayenne.dbsync.reverse.filters.FiltersConfig;
+import org.apache.cayenne.dbsync.reverse.filters.SchemaFilter;
+import org.apache.cayenne.dbsync.reverse.filters.TableFilter;
+import org.apache.cayenne.map.DataMap;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.DbJoin;
+import org.apache.cayenne.map.DbRelationship;
+import org.apache.cayenne.map.DbRelationshipDetected;
+import org.apache.cayenne.map.ObjEntity;
+import org.apache.cayenne.map.Procedure;
+import org.apache.cayenne.map.ProcedureParameter;
+import org.apache.cayenne.map.naming.DefaultUniqueNameGenerator;
+import org.apache.cayenne.map.naming.ExportedKey;
+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.EqualsBuilder;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+
+/**
+ * Performs reverse engineering of the database. It can create
+ * DataMaps using database meta data obtained via JDBC driver.
+ *
+ * @since 4.0
+ */
+public class DbLoader {
+
+    private static final Log LOGGER = LogFactory.getLog(DbLoader.class);
+
+    private static final String WILDCARD = "%";
+
+    private final Connection connection;
+    private final DbAdapter adapter;
+    private final DbLoaderDelegate delegate;
+
+    private boolean creatingMeaningfulPK;
+
+    /**
+     * Strategy for choosing names for entities, attributes and relationships
+     */
+    private ObjectNameGenerator nameGenerator;
+
+    private DatabaseMetaData metaData;
+
+
+    /**
+     * Creates new DbLoader.
+     */
+    public DbLoader(Connection connection, DbAdapter adapter, DbLoaderDelegate delegate) {
+        this(connection, adapter, delegate, new LegacyNameGenerator());
+    }
+
+    /**
+     * Creates new DbLoader with specified naming strategy.
+     *
+     * @since 3.0
+     */
+    public DbLoader(Connection connection, DbAdapter adapter, DbLoaderDelegate delegate, ObjectNameGenerator strategy) {
+        this.adapter = adapter;
+        this.connection = connection;
+        this.delegate = delegate == null ? new DefaultDbLoaderDelegate() : delegate;
+
+        setNameGenerator(strategy);
+    }
+
+    private static List<String> getStrings(ResultSet rs) throws SQLException {
+        List<String> strings = new ArrayList<String>();
+
+        while (rs.next()) {
+            strings.add(rs.getString(1));
+        }
+
+        return strings;
+    }
+
+    private static Collection<ObjEntity> loadObjEntities(DataMap map, DbLoaderConfiguration config,
+                                                         Collection<DbEntity> entities, ObjectNameGenerator nameGenerator) {
+        if (entities.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        Collection<ObjEntity> loadedEntities = new ArrayList<ObjEntity>(entities.size());
+
+        // doLoad empty ObjEntities for all the tables
+        for (DbEntity dbEntity : entities) {
+
+            // check if there are existing entities
+
+            // TODO: performance. This is an O(n^2) search and it shows on
+            // YourKit profiles. Pre-cache mapped entities perhaps (?)
+            Collection<ObjEntity> existing = map.getMappedEntities(dbEntity);
+            if (!existing.isEmpty()) {
+                loadedEntities.addAll(existing);
+                continue;
+            }
+
+            String objEntityName = DefaultUniqueNameGenerator.generate(NameCheckers.objEntity, map,
+                    nameGenerator.createObjEntityName(dbEntity));
+
+            ObjEntity objEntity = new ObjEntity(objEntityName);
+            objEntity.setDbEntity(dbEntity);
+            objEntity.setClassName(config.getGenericClassName() != null ? config.getGenericClassName() : map
+                    .getNameWithDefaultPackage(objEntity.getName()));
+
+            map.addObjEntity(objEntity);
+            loadedEntities.add(objEntity);
+        }
+
+        return loadedEntities;
+    }
+
+    /**
+     * Flattens many-to-many relationships in the generated model.
+     */
+    public static void flattenManyToManyRelationships(DataMap map, Collection<ObjEntity> loadedObjEntities,
+                                                      ObjectNameGenerator objectNameGenerator) {
+        if (loadedObjEntities.isEmpty()) {
+            return;
+        }
+        Collection<ObjEntity> entitiesForDelete = new LinkedList<ObjEntity>();
+
+        for (ObjEntity curEntity : loadedObjEntities) {
+            ManyToManyCandidateEntity entity = ManyToManyCandidateEntity.build(curEntity);
+
+            if (entity != null) {
+                entity.optimizeRelationships(objectNameGenerator);
+                entitiesForDelete.add(curEntity);
+            }
+        }
+
+        // remove needed entities
+        for (ObjEntity curDeleteEntity : entitiesForDelete) {
+            map.removeObjEntity(curDeleteEntity.getName(), true);
+        }
+        loadedObjEntities.removeAll(entitiesForDelete);
+    }
+
+    private static int getDirection(short type) {
+        switch (type) {
+            case DatabaseMetaData.procedureColumnIn:
+                return ProcedureParameter.IN_PARAMETER;
+            case DatabaseMetaData.procedureColumnInOut:
+                return ProcedureParameter.IN_OUT_PARAMETER;
+            case DatabaseMetaData.procedureColumnOut:
+                return ProcedureParameter.OUT_PARAMETER;
+            default:
+                return -1;
+        }
+    }
+
+    /**
+     * Returns DatabaseMetaData object associated with this DbLoader.
+     */
+    private DatabaseMetaData getMetaData() throws SQLException {
+        if (metaData == null) {
+            metaData = connection.getMetaData();
+        }
+        return metaData;
+    }
+
+    /**
+     * Check if database support schemas.
+     */
+    protected boolean supportSchemas() throws SQLException {
+        if (metaData == null) {
+            metaData = connection.getMetaData();
+        }
+        return metaData.supportsSchemasInTableDefinitions();
+    }
+
+    /**
+     * Check if database support catalogs.
+     */
+    protected boolean supportCatalogs() throws SQLException {
+        if (metaData == null) {
+            metaData = connection.getMetaData();
+        }
+        return metaData.supportsCatalogsInTableDefinitions();
+    }
+
+    /**
+     * @since 3.0
+     */
+    public void setCreatingMeaningfulPK(boolean creatingMeaningfulPK) {
+        this.creatingMeaningfulPK = creatingMeaningfulPK;
+    }
+
+    /**
+     * Retrieves catalogs for the database associated with this DbLoader.
+     *
+     * @return List with the catalog names, empty Array if none found.
+     */
+    public List<String> loadCatalogs() throws SQLException {
+        try (ResultSet rs = getMetaData().getCatalogs()) {
+            return getStrings(rs);
+        }
+    }
+
+    /**
+     * Retrieves the schemas for the database.
+     *
+     * @return List with the schema names, empty Array if none found.
+     */
+    public List<String> loadSchemas() throws SQLException {
+
+        try (ResultSet rs = getMetaData().getSchemas()) {
+            return getStrings(rs);
+        }
+    }
+
+    /**
+     * Creates an ObjEntity for each DbEntity in the map.
+     */
+    Collection<ObjEntity> loadObjEntities(DataMap map, DbLoaderConfiguration config,
+                                          Collection<DbEntity> entities) {
+        Collection<ObjEntity> loadedEntities = DbLoader.loadObjEntities(map, config, entities, nameGenerator);
+
+        createEntityMerger(map).synchronizeWithDbEntities(loadedEntities);
+
+        return loadedEntities;
+    }
+
+    /**
+     * @since 4.0
+     */
+    protected EntityMergeSupport createEntityMerger(DataMap map) {
+        return new EntityMergeSupport(map, nameGenerator, !creatingMeaningfulPK);
+    }
+
+    protected void loadDbRelationships(DbLoaderConfiguration config, String catalog, String schema,
+                                       List<DbEntity> tables) throws SQLException {
+        if (config.isSkipRelationshipsLoading()) {
+            return;
+        }
+
+        // Get all the foreign keys referencing this table
+        Map<String, DbEntity> tablesMap = new HashMap<>();
+        for (DbEntity table : tables) {
+            tablesMap.put(table.getName(), table);
+        }
+
+        Map<String, Set<ExportedKey>> keys = loadExportedKeys(config, catalog, schema, tablesMap);
+        for (Map.Entry<String, Set<ExportedKey>> entry : keys.entrySet()) {
+            if (LOGGER.isDebugEnabled()) {
+                LOGGER.debug("Process keys for: " + entry.getKey());
+            }
+
+            Set<ExportedKey> exportedKeys = entry.getValue();
+            ExportedKey key = exportedKeys.iterator().next();
+            if (key == null) {
+                throw new IllegalStateException();
+            }
+
+            DbEntity pkEntity = tablesMap.get(key.getPKTableName());
+            if (pkEntity == null) {
+                skipRelationLog(key, key.getPKTableName());
+                continue;
+            }
+
+            DbEntity fkEntity = tablesMap.get(key.getFKTableName());
+            if (fkEntity == null) {
+                skipRelationLog(key, key.getFKTableName());
+                continue;
+            }
+
+            if (!new EqualsBuilder().append(pkEntity.getCatalog(), key.pkCatalog)
+                    .append(pkEntity.getSchema(), key.pkSchema).append(fkEntity.getCatalog(), key.fkCatalog)
+                    .append(fkEntity.getSchema(), key.fkSchema).isEquals()) {
+
+                LOGGER.info("Skip relation: '" + key + "' because it related to objects from other catalog/schema");
+                LOGGER.info("     relation primary key: '" + key.pkCatalog + "." + key.pkSchema + "'");
+                LOGGER.info("       primary key entity: '" + pkEntity.getCatalog() + "." + pkEntity.getSchema() + "'");
+                LOGGER.info("     relation foreign key: '" + key.fkCatalog + "." + key.fkSchema + "'");
+                LOGGER.info("       foreign key entity: '" + fkEntity.getCatalog() + "." + fkEntity.getSchema() + "'");
+                continue;
+            }
+
+            // forwardRelationship is a reference from table with primary key
+            DbRelationship forwardRelationship = new DbRelationship(generateName(pkEntity, key, true));
+            forwardRelationship.setSourceEntity(pkEntity);
+            forwardRelationship.setTargetEntityName(fkEntity);
+
+            // forwardRelationship is a reference from table with foreign key,
+            // it is what exactly we load from db
+            DbRelationshipDetected reverseRelationship = new DbRelationshipDetected(generateName(fkEntity, key, false));
+            reverseRelationship.setFkName(key.getFKName());
+            reverseRelationship.setSourceEntity(fkEntity);
+            reverseRelationship.setTargetEntityName(pkEntity);
+            reverseRelationship.setToMany(false);
+
+            createAndAppendJoins(exportedKeys, pkEntity, fkEntity, forwardRelationship, reverseRelationship);
+
+            boolean toDependentPK = isToDependentPK(forwardRelationship);
+            forwardRelationship.setToDependentPK(toDependentPK);
+
+            boolean isOneToOne = toDependentPK
+                    && fkEntity.getPrimaryKeys().size() == forwardRelationship.getJoins().size();
+
+            forwardRelationship.setToMany(!isOneToOne);
+            forwardRelationship.setName(generateName(pkEntity, key, !isOneToOne));
+
+            if (delegate.dbRelationshipLoaded(fkEntity, reverseRelationship)) {
+                fkEntity.addRelationship(reverseRelationship);
+            }
+            if (delegate.dbRelationshipLoaded(pkEntity, forwardRelationship)) {
+                pkEntity.addRelationship(forwardRelationship);
+            }
+        }
+    }
+
+    private boolean isToDependentPK(DbRelationship forwardRelationship) {
+        for (DbJoin dbJoin : forwardRelationship.getJoins()) {
+            if (!dbJoin.getTarget().isPrimaryKey()) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    private void createAndAppendJoins(Set<ExportedKey> exportedKeys, DbEntity pkEntity, DbEntity fkEntity,
+                                      DbRelationship forwardRelationship, DbRelationshipDetected reverseRelationship) {
+        for (ExportedKey exportedKey : exportedKeys) {
+            // Create and append joins
+            String pkName = exportedKey.getPKColumnName();
+            String fkName = exportedKey.getFKColumnName();
+
+            // skip invalid joins...
+            DbAttribute pkAtt = pkEntity.getAttribute(pkName);
+            if (pkAtt == null) {
+                LOGGER.info("no attribute for declared primary key: " + pkName);
+                continue;
+            }
+
+            DbAttribute fkAtt = fkEntity.getAttribute(fkName);
+            if (fkAtt == null) {
+                LOGGER.info("no attribute for declared foreign key: " + fkName);
+                continue;
+            }
+
+            forwardRelationship.addJoin(new DbJoin(forwardRelationship, pkName, fkName));
+            reverseRelationship.addJoin(new DbJoin(reverseRelationship, fkName, pkName));
+        }
+    }
+
+    private Map<String, Set<ExportedKey>> loadExportedKeys(DbLoaderConfiguration config, String catalog, String schema,
+                                                           Map<String, DbEntity> tables) throws SQLException {
+        Map<String, Set<ExportedKey>> keys = new HashMap<>();
+
+        for (DbEntity dbEntity : tables.values()) {
+            if (!delegate.dbRelationship(dbEntity)) {
+                continue;
+            }
+
+            ResultSet rs;
+            try {
+                rs = getMetaData().getExportedKeys(catalog, schema, dbEntity.getName());
+            } catch (SQLException cay182Ex) {
+                // Sybase-specific - the line above blows on VIEWS, see CAY-182.
+                LOGGER.info(
+                        "Error getting relationships for '" + catalog + "." + schema + "', ignoring. "
+                                + cay182Ex.getMessage(), cay182Ex);
+                return new HashMap<>();
+            }
+
+            try {
+                while (rs.next()) {
+                    ExportedKey key = ExportedKey.extractData(rs);
+
+                    DbEntity fkEntity = tables.get(key.getFKTableName());
+                    if (fkEntity == null) {
+                        skipRelationLog(key, key.getFKTableName());
+                        continue;
+                    }
+
+                    if (config.getFiltersConfig().tableFilter(fkEntity.getCatalog(), fkEntity.getSchema())
+                            .isIncludeTable(fkEntity.getName()) == null) {
+                        continue;
+                    }
+
+                    Set<ExportedKey> exportedKeys = keys.get(key.getStrKey());
+                    if (exportedKeys == null) {
+                        exportedKeys = new TreeSet<ExportedKey>();
+
+                        keys.put(key.getStrKey(), exportedKeys);
+                    }
+                    exportedKeys.add(key);
+                }
+
+            } finally {
+                rs.close();
+            }
+        }
+        return keys;
+    }
+
+    private void skipRelationLog(ExportedKey key, String tableName) {
+        LOGGER.info("Skip relation: '" + key + "' because table '" + tableName + "' not found");
+    }
+
+    private String generateName(DbEntity entity, ExportedKey key, boolean toMany) {
+        String forwardPreferredName = nameGenerator.createDbRelationshipName(key, toMany);
+        return DefaultUniqueNameGenerator.generate(NameCheckers.dbRelationship, entity, forwardPreferredName);
+    }
+
+    private void fireObjEntitiesAddedEvents(Collection<ObjEntity> loadedObjEntities) {
+        for (ObjEntity curEntity : loadedObjEntities) {
+            // notify delegate
+            if (delegate != null) {
+                delegate.objEntityAdded(curEntity);
+            }
+        }
+    }
+
+    protected String[] getTableTypes(DbLoaderConfiguration config) {
+
+        String[] configTypes = config.getTableTypes();
+        if (configTypes != null && configTypes.length > 0) {
+            return configTypes;
+        }
+
+        List<String> list = new ArrayList<>(2);
+
+        String viewType = adapter.tableTypeForView();
+        if (viewType != null) {
+            list.add(viewType);
+        }
+
+        String tableType = adapter.tableTypeForTable();
+        if (tableType != null) {
+            list.add(tableType);
+        }
+
+        return list.toArray(new String[list.size()]);
+    }
+
+    /**
+     * Performs database reverse engineering based on the specified config and
+     * fills the specified DataMap object with DB and object mapping info.
+     *
+     * @since 4.0
+     */
+    public void load(DataMap dataMap, DbLoaderConfiguration config) throws SQLException {
+        LOGGER.info("Schema loading...");
+
+        String[] types = getTableTypes(config);
+
+        for (CatalogFilter catalog : config.getFiltersConfig().catalogs) {
+            for (SchemaFilter schema : catalog.schemas) {
+
+                List<DbEntity> entities = createTableLoader(catalog.name, schema.name, schema.tables).loadDbEntities(
+                        dataMap, config, types);
+
+                if (entities != null) {
+                    loadDbRelationships(config, catalog.name, schema.name, entities);
+
+                    prepareObjLayer(dataMap, config, entities);
+                }
+            }
+        }
+    }
+
+    protected DbTableLoader createTableLoader(String catalog, String schema, TableFilter filter) throws SQLException {
+        return new DbTableLoader(catalog, schema, getMetaData(), delegate, new DbAttributesPerSchemaLoader(catalog,
+                schema, getMetaData(), adapter, filter));
+    }
+
+    private void prepareObjLayer(DataMap dataMap, DbLoaderConfiguration config, Collection<DbEntity> entities) {
+        Collection<ObjEntity> loadedObjEntities = loadObjEntities(dataMap, config, entities);
+        flattenManyToManyRelationships(dataMap, loadedObjEntities, nameGenerator);
+        fireObjEntitiesAddedEvents(loadedObjEntities);
+    }
+
+    /**
+     * Performs database reverse engineering to match the specified catalog,
+     * schema, table name and table type patterns and fills the specified
+     * DataMap object with DB and object mapping info.
+     *
+     * @since 4.0
+     */
+    public DataMap load(DbLoaderConfiguration config) throws SQLException {
+
+        DataMap dataMap = new DataMap();
+        load(dataMap, config);
+        loadProcedures(dataMap, config);
+
+        return dataMap;
+    }
+
+    /**
+     * Loads database stored procedures into the DataMap.
+     * <p>
+     * <i>As of 1.1 there is no boolean property or delegate method to make
+     * procedure loading optional or to implement custom merging logic, so
+     * currently this method is NOT CALLED from "loadDataMapFromDB" and should
+     * be invoked explicitly by the user. </i>
+     * </p>
+     *
+     * @since 4.0
+     */
+    public Map<String, Procedure> loadProcedures(DataMap dataMap, DbLoaderConfiguration config) throws SQLException {
+
+        Map<String, Procedure> procedures = loadProcedures(config);
+        if (procedures.isEmpty()) {
+            return procedures;
+        }
+
+        loadProceduresColumns(config, procedures);
+
+        for (Procedure procedure : procedures.values()) {
+            dataMap.addProcedure(procedure);
+        }
+
+        return procedures;
+    }
+
+    private void loadProceduresColumns(DbLoaderConfiguration config, Map<String, Procedure> procedures)
+            throws SQLException {
+
+        for (CatalogFilter catalog : config.getFiltersConfig().catalogs) {
+            for (SchemaFilter schema : catalog.schemas) {
+                loadProceduresColumns(procedures, catalog.name, schema.name);
+            }
+        }
+    }
+
+    private void loadProceduresColumns(Map<String, Procedure> procedures, String catalog, String schema)
+            throws SQLException {
+
+        try (ResultSet columnsRS = getMetaData().getProcedureColumns(catalog, schema, null, null);) {
+            while (columnsRS.next()) {
+
+                String s = columnsRS.getString("PROCEDURE_SCHEM");
+                String name = columnsRS.getString("PROCEDURE_NAME");
+                String key = (s == null ? "" : s + '.') + name;
+                Procedure procedure = procedures.get(key);
+                if (procedure == null) {
+                    continue;
+                }
+
+                ProcedureParameter column = loadProcedureParams(columnsRS, key, procedure);
+                if (column == null) {
+                    continue;
+                }
+                procedure.addCallParameter(column);
+            }
+        }
+    }
+
+    private ProcedureParameter loadProcedureParams(ResultSet columnsRS, String key, Procedure procedure)
+            throws SQLException {
+        String columnName = columnsRS.getString("COLUMN_NAME");
+
+        // skip ResultSet columns, as they are not described in Cayenne
+        // procedures yet...
+        short type = columnsRS.getShort("COLUMN_TYPE");
+        if (type == DatabaseMetaData.procedureColumnResult) {
+            LOGGER.debug("skipping ResultSet column: " + key + "." + columnName);
+        }
+
+        if (columnName == null) {
+            if (type == DatabaseMetaData.procedureColumnReturn) {
+                LOGGER.debug("null column name, assuming result column: " + key);
+                columnName = "_return_value";
+                procedure.setReturningValue(true);
+            } else {
+                LOGGER.info("invalid null column name, skipping column : " + key);
+                return null;
+            }
+        }
+
+        int columnType = columnsRS.getInt("DATA_TYPE");
+
+        // ignore precision of non-decimal columns
+        int decimalDigits = -1;
+        if (TypesMapping.isDecimal(columnType)) {
+            decimalDigits = columnsRS.getShort("SCALE");
+            if (columnsRS.wasNull()) {
+                decimalDigits = -1;
+            }
+        }
+
+        ProcedureParameter column = new ProcedureParameter(columnName);
+        int direction = getDirection(type);
+        if (direction != -1) {
+            column.setDirection(direction);
+        }
+
+        column.setType(columnType);
+        column.setMaxLength(columnsRS.getInt("LENGTH"));
+        column.setPrecision(decimalDigits);
+
+        column.setProcedure(procedure);
+        return column;
+    }
+
+    private Map<String, Procedure> loadProcedures(DbLoaderConfiguration config) throws SQLException {
+        Map<String, Procedure> procedures = new HashMap<>();
+
+        FiltersConfig filters = config.getFiltersConfig();
+        for (CatalogFilter catalog : filters.catalogs) {
+            for (SchemaFilter schema : catalog.schemas) {
+                if (filters.proceduresFilter(catalog.name, schema.name).isEmpty()) {
+                    continue;
+                }
+
+                procedures.putAll(loadProcedures(filters, catalog.name, schema.name));
+            }
+        }
+
+        return procedures;
+    }
+
+    private Map<String, Procedure> loadProcedures(FiltersConfig filters, String catalog, String schema)
+            throws SQLException {
+        Map<String, Procedure> procedures = new HashMap<>();
+        // get procedures
+
+        try (ResultSet rs = getMetaData().getProcedures(catalog, schema, WILDCARD);) {
+            while (rs.next()) {
+
+                String name = rs.getString("PROCEDURE_NAME");
+                Procedure procedure = new Procedure(name);
+                procedure.setCatalog(rs.getString("PROCEDURE_CAT"));
+                procedure.setSchema(rs.getString("PROCEDURE_SCHEM"));
+
+                if (!filters.proceduresFilter(procedure.getCatalog(), procedure.getSchema()).isInclude(
+                        procedure.getName())) {
+                    LOGGER.info("skipping Cayenne PK procedure: " + name);
+                    continue;
+                }
+
+                switch (rs.getShort("PROCEDURE_TYPE")) {
+                    case DatabaseMetaData.procedureNoResult:
+                    case DatabaseMetaData.procedureResultUnknown:
+                        procedure.setReturningValue(false);
+                        break;
+                    case DatabaseMetaData.procedureReturnsResult:
+                        procedure.setReturningValue(true);
+                        break;
+                }
+
+                procedures.put(procedure.getFullyQualifiedName(), procedure);
+            }
+        }
+        return procedures;
+    }
+
+    /**
+     * Sets new naming strategy for reverse engineering
+     *
+     * @since 3.0
+     */
+    public void setNameGenerator(ObjectNameGenerator strategy) {
+        if (strategy == null) {
+            LOGGER.warn("Attempt to set null into NameGenerator. LegacyNameGenerator will be used.");
+            this.nameGenerator = new LegacyNameGenerator();
+        } else {
+            this.nameGenerator = strategy;
+        }
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/cayenne/blob/cf172fc9/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/db/DbLoaderConfiguration.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/db/DbLoaderConfiguration.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/db/DbLoaderConfiguration.java
new file mode 100644
index 0000000..f3adbd1
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/db/DbLoaderConfiguration.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.reverse.db;
+
+import org.apache.cayenne.dbsync.reverse.filters.TableFilter;
+import org.apache.cayenne.dbsync.reverse.filters.FiltersConfig;
+import org.apache.cayenne.dbsync.reverse.filters.PatternFilter;
+
+/**
+ * @since 4.0
+ */
+public class DbLoaderConfiguration {
+
+    /**
+     * Returns a name of a generic class that should be used for all
+     * ObjEntities. The most common generic class is
+     * {@link org.apache.cayenne.CayenneDataObject}. If generic class name is
+     * null (which is the default), DbLoader will assign each entity a unique
+     * class name derived from the table name.
+     *
+     */
+    private String genericClassName;
+
+    /**
+     * Java class implementing org.apache.cayenne.map.naming.NamingStrategy.
+     * This is used to specify how ObjEntities will be mapped from the imported
+     * DB schema.
+     */
+    private String namingStrategy;
+
+    private Boolean skipRelationshipsLoading;
+
+    private Boolean skipPrimaryKeyLoading;
+
+    private String[] tableTypes;
+
+    private FiltersConfig filtersConfig;
+
+    public String getGenericClassName() {
+        return genericClassName;
+    }
+
+    public void setGenericClassName(String genericClassName) {
+        this.genericClassName = genericClassName;
+    }
+
+    public String[] getTableTypes() {
+        return tableTypes;
+    }
+
+    public void setTableTypes(String[] tableTypes) {
+        this.tableTypes = tableTypes;
+    }
+
+    public String getNamingStrategy() {
+        return namingStrategy;
+    }
+
+    public void setNamingStrategy(String namingStrategy) {
+        this.namingStrategy = namingStrategy;
+    }
+
+    public FiltersConfig getFiltersConfig() {
+        if (filtersConfig == null) {
+            // this case is used often in tests where config not initialized properly
+            return FiltersConfig.create(null, null, TableFilter.everything(), PatternFilter.INCLUDE_NOTHING);
+        }
+        return filtersConfig;
+    }
+
+    public void setFiltersConfig(FiltersConfig filtersConfig) {
+        this.filtersConfig = filtersConfig;
+    }
+
+    public boolean isSkipRelationshipsLoading() {
+        return skipRelationshipsLoading != null && skipRelationshipsLoading;
+    }
+
+    public Boolean getSkipRelationshipsLoading() {
+        return skipRelationshipsLoading;
+    }
+
+    public void setSkipRelationshipsLoading(Boolean skipRelationshipsLoading) {
+        this.skipRelationshipsLoading = skipRelationshipsLoading;
+    }
+
+    public void setSkipPrimaryKeyLoading(Boolean skipPrimaryKeyLoading) {
+        this.skipPrimaryKeyLoading = skipPrimaryKeyLoading;
+    }
+
+    public boolean getSkipPrimaryKeyLoading() {
+        return skipPrimaryKeyLoading;
+    }
+
+    public boolean isSkipPrimaryKeyLoading() {
+        return skipPrimaryKeyLoading != null && skipPrimaryKeyLoading;
+    }
+
+    @Override
+    public String toString() {
+        String res = "EntitiesFilters: " + getFiltersConfig();
+        if (isSkipRelationshipsLoading()) {
+            res += "\n Skip Loading Relationships! \n";
+        }
+
+        if (isSkipPrimaryKeyLoading()) {
+            res += "\n Skip Loading PrimaryKeys! \n";
+        }
+
+        return res;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/cf172fc9/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/db/DbLoaderDelegate.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/db/DbLoaderDelegate.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/db/DbLoaderDelegate.java
new file mode 100644
index 0000000..e85059f
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/db/DbLoaderDelegate.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.reverse.db;
+
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.DbRelationship;
+import org.apache.cayenne.map.ObjEntity;
+
+/**
+ * DbLoaderDelegate defines API that allows to control the behavior of DbLoader
+ * during the database reverse-engineering. Delegate is also notified of the
+ * progress of reverse-engineering.
+ */
+public interface DbLoaderDelegate {
+
+    void dbEntityAdded(DbEntity entity);
+
+    void dbEntityRemoved(DbEntity entity);
+
+    /**
+     * Called before relationship loading for db-entity
+     * @param entity
+     *
+     * @return true in case you want process relationships for this entity
+     *         false otherwise
+     */
+    boolean dbRelationship(DbEntity entity);
+
+    /**
+     * Called before relationship will be added into db-entity but after it was loaded from db
+     * @param entity
+     *
+     * @return true in case you want add this relationship into entity
+     *         false otherwise
+     */
+    boolean dbRelationshipLoaded(DbEntity entity, DbRelationship relationship);
+
+    void objEntityAdded(ObjEntity entity);
+
+    void objEntityRemoved(ObjEntity entity);
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/cf172fc9/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/db/DbTableLoader.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/db/DbTableLoader.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/db/DbTableLoader.java
new file mode 100644
index 0000000..d1230bd
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/db/DbTableLoader.java
@@ -0,0 +1,195 @@
+/*****************************************************************
+ *   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.db;
+
+import java.sql.DatabaseMetaData;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.apache.cayenne.dbsync.reverse.filters.PatternFilter;
+import org.apache.cayenne.dbsync.reverse.filters.TableFilter;
+import org.apache.cayenne.map.DataMap;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.DetectedDbEntity;
+import org.apache.cayenne.map.ObjEntity;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * @since 4.0
+ */
+public class DbTableLoader {
+
+	private static final Log LOGGER = LogFactory.getLog(DbTableLoader.class);
+
+	private static final String WILDCARD = "%";
+
+	private final String catalog;
+	private final String schema;
+
+	private final DatabaseMetaData metaData;
+	private final DbLoaderDelegate delegate;
+
+	private final DbAttributesLoader attributesLoader;
+
+	public DbTableLoader(String catalog, String schema, DatabaseMetaData metaData, DbLoaderDelegate delegate,
+			DbAttributesLoader attributesLoader) {
+		this.catalog = catalog;
+		this.schema = schema;
+		this.metaData = metaData;
+		this.delegate = delegate;
+
+		this.attributesLoader = attributesLoader;
+	}
+
+	/**
+	 * Returns all tables for given combination of the criteria. Tables returned
+	 * as DbEntities without any attributes or relationships.
+	 *
+	 * @param types
+	 *            The types of table names to retrieve, null returns all types.
+	 * @return
+	 * @since 4.0
+	 */
+	public List<DetectedDbEntity> getDbEntities(TableFilter filters, String[] types) throws SQLException {
+		if (LOGGER.isDebugEnabled()) {
+			LOGGER.debug("Read tables: catalog=" + catalog + ", schema=" + schema + ", types=" + Arrays.toString(types));
+		}
+
+		List<DetectedDbEntity> tables = new LinkedList<DetectedDbEntity>();
+		try (ResultSet rs = metaData.getTables(catalog, schema, WILDCARD, types);) {
+			while (rs.next()) {
+				// Oracle 9i and newer has a nifty recycle bin feature... but we
+				// don't
+				// want dropped tables to be included here; in fact they may
+				// even result
+				// in errors on reverse engineering as their names have special
+				// chars like
+				// "/", etc. So skip them all together
+
+				String name = rs.getString("TABLE_NAME");
+				if (name == null) {
+					continue;
+				}
+
+				DetectedDbEntity table = new DetectedDbEntity(name);
+
+				String catalog = rs.getString("TABLE_CAT");
+				table.setCatalog(catalog);
+
+				String schema = rs.getString("TABLE_SCHEM");
+				table.setSchema(schema);
+				if (!(this.catalog == null || this.catalog.equals(catalog))
+						|| !(this.schema == null || this.schema.equals(schema))) {
+
+					LOGGER.error(catalog + "." + schema + "." + name + " wrongly loaded for catalog/schema : "
+							+ this.catalog + "." + this.schema);
+
+					continue;
+				}
+
+				PatternFilter includeTable = filters.isIncludeTable(table.getName());
+				if (includeTable != null) {
+					tables.add(table);
+				}
+			}
+		}
+		return tables;
+	}
+
+	/**
+	 * Loads dbEntities for the specified tables.
+	 * 
+	 * @param config
+	 * @param types
+	 */
+	public List<DbEntity> loadDbEntities(DataMap map, DbLoaderConfiguration config, String[] types) throws SQLException {
+		/** List of db entities to process. */
+
+		List<DetectedDbEntity> tables = getDbEntities(config.getFiltersConfig().tableFilter(catalog, schema), types);
+
+		List<DbEntity> dbEntities = new ArrayList<DbEntity>();
+		for (DbEntity dbEntity : tables) {
+			DbEntity oldEnt = map.getDbEntity(dbEntity.getName());
+			if (oldEnt != null) {
+				Collection<ObjEntity> oldObjEnt = map.getMappedEntities(oldEnt);
+				if (!oldObjEnt.isEmpty()) {
+					for (ObjEntity objEntity : oldObjEnt) {
+						LOGGER.debug("Delete ObjEntity: " + objEntity.getName());
+						map.removeObjEntity(objEntity.getName(), true);
+						delegate.objEntityRemoved(objEntity);
+					}
+				}
+
+				LOGGER.debug("Overwrite DbEntity: " + oldEnt.getName());
+				map.removeDbEntity(oldEnt.getName(), true);
+				delegate.dbEntityRemoved(oldEnt);
+			}
+
+			map.addDbEntity(dbEntity);
+
+			delegate.dbEntityAdded(dbEntity);
+
+			// delegate might have thrown this entity out... so check if it is
+			// still
+			// around before continuing processing
+			if (map.getDbEntity(dbEntity.getName()) == dbEntity) {
+				dbEntities.add(dbEntity);
+				attributesLoader.loadDbAttributes(dbEntity);
+				if (!config.isSkipPrimaryKeyLoading()) {
+					loadPrimaryKey(dbEntity);
+				}
+			}
+		}
+
+		return dbEntities;
+	}
+
+	private void loadPrimaryKey(DbEntity dbEntity) throws SQLException {
+
+		try (ResultSet rs = metaData.getPrimaryKeys(dbEntity.getCatalog(), dbEntity.getSchema(), dbEntity.getName());) {
+			while (rs.next()) {
+				String columnName = rs.getString("COLUMN_NAME");
+				DbAttribute attribute = dbEntity.getAttribute(columnName);
+
+				if (attribute != null) {
+					attribute.setPrimaryKey(true);
+				} else {
+					// why an attribute might be null is not quiet clear
+					// but there is a bug report 731406 indicating that it is
+					// possible
+					// so just print the warning, and ignore
+					LOGGER.warn("Can't locate attribute for primary key: " + columnName);
+				}
+
+				String pkName = rs.getString("PK_NAME");
+				if (pkName != null && dbEntity instanceof DetectedDbEntity) {
+					((DetectedDbEntity) dbEntity).setPrimaryKeyName(pkName);
+				}
+
+			}
+		}
+	}
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/cf172fc9/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/db/DefaultDbLoaderDelegate.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/db/DefaultDbLoaderDelegate.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/db/DefaultDbLoaderDelegate.java
new file mode 100644
index 0000000..cf336df
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/db/DefaultDbLoaderDelegate.java
@@ -0,0 +1,59 @@
+/*****************************************************************
+ *   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.db;
+
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.DbRelationship;
+import org.apache.cayenne.map.ObjEntity;
+
+/**
+ * @since 4.0.
+ */
+public class DefaultDbLoaderDelegate implements DbLoaderDelegate {
+
+    @Override
+    public void dbEntityAdded(DbEntity entity) {
+
+    }
+
+    @Override
+    public void dbEntityRemoved(DbEntity entity) {
+
+    }
+
+    @Override
+    public boolean dbRelationship(DbEntity entity) {
+        return true;
+    }
+
+    @Override
+    public boolean dbRelationshipLoaded(DbEntity entity, DbRelationship relationship) {
+        return true;
+    }
+
+    @Override
+    public void objEntityAdded(ObjEntity entity) {
+
+    }
+
+    @Override
+    public void objEntityRemoved(ObjEntity entity) {
+
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/cf172fc9/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/db/LoggingDbLoaderDelegate.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/db/LoggingDbLoaderDelegate.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/db/LoggingDbLoaderDelegate.java
new file mode 100644
index 0000000..3a9a905
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/db/LoggingDbLoaderDelegate.java
@@ -0,0 +1,76 @@
+/*
+ * 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.db;
+
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.DbRelationship;
+import org.apache.cayenne.map.ObjEntity;
+import org.apache.commons.logging.Log;
+
+/**
+ * @since 4.0
+ */
+public class LoggingDbLoaderDelegate extends DefaultDbLoaderDelegate {
+
+    private final Log logger;
+
+    public LoggingDbLoaderDelegate(Log logger) {
+        this.logger = logger;
+    }
+
+    @Override
+    public void dbEntityAdded(DbEntity entity) {
+        logger.info("  Table: " + entity.getFullyQualifiedName());
+    }
+
+    @Override
+    public void dbEntityRemoved(DbEntity entity) {
+        logger.info("  Table removed: " + entity.getFullyQualifiedName());
+    }
+
+    @Override
+    public boolean dbRelationship(DbEntity entity) {
+        if (logger.isDebugEnabled()) {
+            logger.debug("    Relationships for " + entity.getFullyQualifiedName());
+        }
+
+        return true;
+    }
+
+    @Override
+    public boolean dbRelationshipLoaded(DbEntity entity, DbRelationship relationship) {
+        logger.info("    " + relationship);
+
+        return true;
+    }
+
+    @Override
+    public void objEntityAdded(ObjEntity entity) {
+        if (logger.isDebugEnabled()) {
+            logger.debug("  Class: " + entity.getName());
+        }
+    }
+
+    @Override
+    public void objEntityRemoved(ObjEntity entity) {
+        if (logger.isDebugEnabled()) {
+            logger.debug("  Class removed: " + entity.getName());
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/cf172fc9/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/db/ManyToManyCandidateEntity.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/db/ManyToManyCandidateEntity.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/db/ManyToManyCandidateEntity.java
new file mode 100644
index 0000000..f185619
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/db/ManyToManyCandidateEntity.java
@@ -0,0 +1,143 @@
+/*****************************************************************
+ *   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.db;
+
+import org.apache.cayenne.map.DbRelationship;
+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.ExportedKey;
+import org.apache.cayenne.map.naming.NameCheckers;
+import org.apache.cayenne.map.naming.ObjectNameGenerator;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Class represent ObjEntity that may be optimized using flattened relationships
+ * as many to many table
+ */
+class ManyToManyCandidateEntity {
+
+    private static final Log LOG = LogFactory.getLog(ManyToManyCandidateEntity.class);
+
+    private final ObjEntity joinEntity;
+
+    private final DbRelationship dbRel1;
+    private final DbRelationship dbRel2;
+
+    private final ObjEntity entity1;
+    private final ObjEntity entity2;
+
+    private final DbRelationship reverseRelationship1;
+    private final DbRelationship reverseRelationship2;
+
+    private ManyToManyCandidateEntity(ObjEntity entityValue, List<ObjRelationship> relationships) {
+        joinEntity = entityValue;
+
+        ObjRelationship rel1 = relationships.get(0);
+        ObjRelationship rel2 = relationships.get(1);
+
+        dbRel1 = rel1.getDbRelationships().get(0);
+        dbRel2 = rel2.getDbRelationships().get(0);
+
+        reverseRelationship1 = dbRel1.getReverseRelationship();
+        reverseRelationship2 = dbRel2.getReverseRelationship();
+
+        entity1 = rel1.getTargetEntity();
+        entity2 = rel2.getTargetEntity();
+    }
+
+    /**
+     * Method check - if current entity represent many to many temporary table
+     *
+     * @return true if current entity is represent many to many table; otherwise returns false
+     */
+    public static ManyToManyCandidateEntity build(ObjEntity joinEntity) {
+        ArrayList<ObjRelationship> relationships = new ArrayList<ObjRelationship>(joinEntity.getRelationships());
+        if (relationships.size() != 2 || (relationships.get(0).getDbRelationships().isEmpty() || relationships.get(1).getDbRelationships().isEmpty())) {
+            return null;
+        }
+
+        ManyToManyCandidateEntity candidateEntity = new ManyToManyCandidateEntity(joinEntity, relationships);
+        if (candidateEntity.isManyToMany()) {
+            return candidateEntity;
+        }
+
+        return null;
+    }
+
+    private boolean isManyToMany() {
+        boolean isNotHaveAttributes = joinEntity.getAttributes().size() == 0;
+
+        return isNotHaveAttributes
+                && reverseRelationship1 != null && reverseRelationship1.isToDependentPK()
+                && reverseRelationship2 != null && reverseRelationship2.isToDependentPK()
+                && entity1 != null && entity2 != null;
+    }
+
+    private void addFlattenedRelationship(ObjectNameGenerator nameGenerator, ObjEntity srcEntity, ObjEntity dstEntity,
+                                          DbRelationship rel1, DbRelationship rel2) {
+
+        if (rel1.getSourceAttributes().isEmpty() && rel2.getTargetAttributes().isEmpty()) {
+            LOG.warn("Wrong call ManyToManyCandidateEntity.addFlattenedRelationship(... , " + srcEntity.getName()
+                    + ", " + dstEntity.getName() + ", ...)");
+
+            return;
+        }
+
+        ExportedKey key = new ExportedKey(
+                rel1.getSourceEntity().getName(),
+                rel1.getSourceAttributes().iterator().next().getName(),
+                null,
+                rel2.getTargetEntity().getName(),
+                rel2.getTargetAttributes().iterator().next().getName(),
+                null,
+                (short) 1);
+
+        ObjRelationship newRelationship = new ObjRelationship();
+        newRelationship.setName(DefaultUniqueNameGenerator.generate(NameCheckers.objRelationship, srcEntity,
+                nameGenerator.createDbRelationshipName(key, true)));
+
+        newRelationship.setSourceEntity(srcEntity);
+        newRelationship.setTargetEntityName(dstEntity);
+
+        newRelationship.addDbRelationship(rel1);
+        newRelationship.addDbRelationship(rel2);
+
+        srcEntity.addRelationship(newRelationship);
+    }
+
+    /**
+     * Method make direct relationships between 2 entities and remove relationships to
+     * many to many entity
+     *
+     * @param nameGenerator
+     */
+    public void optimizeRelationships(ObjectNameGenerator nameGenerator) {
+        entity1.removeRelationship(reverseRelationship1.getName());
+        entity2.removeRelationship(reverseRelationship2.getName());
+
+        addFlattenedRelationship(nameGenerator, entity1, entity2, reverseRelationship1, dbRel2);
+        addFlattenedRelationship(nameGenerator, entity2, entity1, reverseRelationship2, dbRel1);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/cf172fc9/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/DbMergerTest.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/DbMergerTest.java b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/DbMergerTest.java
index 8cb03dd..a695d2d 100644
--- a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/DbMergerTest.java
+++ b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/DbMergerTest.java
@@ -20,7 +20,7 @@ package org.apache.cayenne.dbsync.merge;
 
 import org.apache.cayenne.dbsync.merge.builders.DbEntityBuilder;
 import org.apache.cayenne.dbsync.merge.factory.HSQLMergerTokenFactory;
-import org.apache.cayenne.dbsync.reverse.DbLoaderConfiguration;
+import org.apache.cayenne.dbsync.reverse.db.DbLoaderConfiguration;
 import org.apache.cayenne.map.DataMap;
 import org.apache.cayenne.map.DbEntity;
 import org.junit.Test;

http://git-wip-us.apache.org/repos/asf/cayenne/blob/cf172fc9/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
index c2357b0..ac68802 100644
--- 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
@@ -23,7 +23,7 @@ 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.db.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;

http://git-wip-us.apache.org/repos/asf/cayenne/blob/cf172fc9/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
deleted file mode 100644
index 93539ed..0000000
--- a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/reverse/DbLoaderIT.java
+++ /dev/null
@@ -1,434 +0,0 @@
-/*****************************************************************
- *   Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- ****************************************************************/
-
-package org.apache.cayenne.dbsync.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.Connection;
-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;
-
-    private Connection connection;
-
-    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 + "]";
-    }
-
-    @Before
-    public void before() throws Exception {
-        this.connection = dataSourceFactory.getSharedDataSource().getConnection();
-        this.loader = new DbLoader(connection, adapter, null);
-    }
-
-    @After
-    public void after() throws Exception {
-        connection.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());
-            }
-        }
-    }
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/cf172fc9/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/reverse/FiltersConfigBuilderTest.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/reverse/FiltersConfigBuilderTest.java b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/reverse/FiltersConfigBuilderTest.java
index 6d945a5..5b1a0e7 100644
--- a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/reverse/FiltersConfigBuilderTest.java
+++ b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/reverse/FiltersConfigBuilderTest.java
@@ -272,7 +272,7 @@ public class FiltersConfigBuilderTest {
     /*@Test
     public void testEmptyDbEntitiesFilters() throws Exception {
         ReverseEngineering engineering = new ReverseEngineering();
-        FiltersConfig executions = new FiltersConfigBuilder(engineering).filtersConfig();
+        FiltersConfig executions = new FiltersConfigBuilder(engineering).build();
 
         assertEquals("If nothing was configured we have to import everything. Filter %/%/% true/true/true",
                 new FiltersConfig(eFilters(path(), TRUE, TRUE, NULL)),
@@ -283,7 +283,7 @@ public class FiltersConfigBuilderTest {
     public void testOnlyOneCatalogDbEntitiesFilters() throws Exception {
         ReverseEngineering engineering = new ReverseEngineering();
         engineering.addCatalog(new Catalog("catalog_01"));
-        FiltersConfig executions = new FiltersConfigBuilder(engineering).filtersConfig();
+        FiltersConfig executions = new FiltersConfigBuilder(engineering).build();
 
 
         assertEquals(new FiltersConfig(eFilters(path("catalog_01", null), TRUE, TRUE, NULL)),
@@ -301,7 +301,7 @@ public class FiltersConfigBuilderTest {
         engineering.addCatalog(new Catalog("catalog_03").schema(new Schema("schema_01")));
         engineering.addCatalog(new Catalog("catalog_03").schema(new Schema("schema_01")));
         engineering.addCatalog(new Catalog("catalog_03").schema(new Schema("schema_01")));
-        FiltersConfig executions = new FiltersConfigBuilder(engineering).filtersConfig();
+        FiltersConfig executions = new FiltersConfigBuilder(engineering).build();
 
 
         assertEquals(new FiltersConfig(
@@ -320,7 +320,7 @@ public class FiltersConfigBuilderTest {
         engineering.addSchema(new Schema("schema_01"));
         engineering.addSchema(new Schema("schema_02"));
         engineering.addSchema(new Schema("schema_03"));
-        FiltersConfig executions = new FiltersConfigBuilder(engineering).filtersConfig();
+        FiltersConfig executions = new FiltersConfigBuilder(engineering).build();
 
 
         assertEquals(new FiltersConfig(
@@ -341,7 +341,7 @@ public class FiltersConfigBuilderTest {
         engineering.addExcludeColumn(new ExcludeColumn("ExcludeColumn"));
         engineering.addExcludeProcedure(new ExcludeProcedure("ExcludeProcedure"));
 
-        FiltersConfig executions = new FiltersConfigBuilder(engineering).filtersConfig();
+        FiltersConfig executions = new FiltersConfigBuilder(engineering).build();
 
         assertEquals(new FiltersConfig(
                         eFilters(path(),
@@ -367,7 +367,7 @@ public class FiltersConfigBuilderTest {
         ReverseEngineering engineering = new ReverseEngineering();
         engineering.addCatalog(catalog);
 
-        FiltersConfig executions = new FiltersConfigBuilder(engineering).filtersConfig();
+        FiltersConfig executions = new FiltersConfigBuilder(engineering).build();
 
         assertEquals(new FiltersConfig(
                         eFilters(path("catalog", "schema"), include("table"), NULL, NULL),
@@ -385,7 +385,7 @@ public class FiltersConfigBuilderTest {
         builder.add(new EntityFilters(path, NULL, NULL, NULL));
         builder.add(new EntityFilters(path, NULL, NULL, NULL));
 
-        EntityFilters filter = builder.filtersConfig().filter(path);
+        EntityFilters filter = builder.build().filter(path);
         assertFalse(filter.isEmpty());
     }*/
 }
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/cayenne/blob/cf172fc9/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/reverse/ManyToManyCandidateEntityTest.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/reverse/ManyToManyCandidateEntityTest.java b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/reverse/ManyToManyCandidateEntityTest.java
deleted file mode 100644
index edecdfc..0000000
--- a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/reverse/ManyToManyCandidateEntityTest.java
+++ /dev/null
@@ -1,113 +0,0 @@
-/*****************************************************************
- *   Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- ****************************************************************/
-package org.apache.cayenne.dbsync.reverse;
-
-import org.apache.cayenne.configuration.ConfigurationNameMapper;
-import org.apache.cayenne.configuration.ConfigurationTree;
-import org.apache.cayenne.configuration.DataChannelDescriptor;
-import org.apache.cayenne.configuration.DataMapLoader;
-import org.apache.cayenne.configuration.DefaultConfigurationNameMapper;
-import org.apache.cayenne.configuration.XMLDataChannelDescriptorLoader;
-import org.apache.cayenne.configuration.XMLDataMapLoader;
-import org.apache.cayenne.di.AdhocObjectFactory;
-import org.apache.cayenne.di.Binder;
-import org.apache.cayenne.di.ClassLoaderManager;
-import org.apache.cayenne.di.DIBootstrap;
-import org.apache.cayenne.di.Injector;
-import org.apache.cayenne.di.Module;
-import org.apache.cayenne.di.spi.DefaultAdhocObjectFactory;
-import org.apache.cayenne.di.spi.DefaultClassLoaderManager;
-import org.apache.cayenne.map.DataMap;
-import org.apache.cayenne.map.ObjEntity;
-import org.apache.cayenne.map.Relationship;
-import org.apache.cayenne.map.naming.LegacyNameGenerator;
-import org.apache.cayenne.resource.URLResource;
-import org.junit.Before;
-import org.junit.Test;
-
-import java.net.URL;
-import java.util.ArrayList;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-
-public class ManyToManyCandidateEntityTest {
-
-    private DataMap map;
-
-    @Before
-    public void setUp() throws Exception {
-        Module testModule = new Module() {
-
-            public void configure(Binder binder) {
-                binder.bind(ClassLoaderManager.class).to(DefaultClassLoaderManager.class);
-                binder.bind(AdhocObjectFactory.class).to(DefaultAdhocObjectFactory.class);
-                binder.bind(DataMapLoader.class).to(XMLDataMapLoader.class);
-                binder.bind(ConfigurationNameMapper.class).to(DefaultConfigurationNameMapper.class);
-            }
-        };
-
-        Injector injector = DIBootstrap.createInjector(testModule);
-
-        // create and initialize loader instance to test
-        XMLDataChannelDescriptorLoader loader = new XMLDataChannelDescriptorLoader();
-        injector.injectMembers(loader);
-
-        String testConfigName = "relationship-optimisation";
-        URL url = getClass().getResource("cayenne-" + testConfigName + ".xml");
-
-        ConfigurationTree<DataChannelDescriptor> tree = loader.load(new URLResource(url));
-
-        map = tree.getRootNode().getDataMap(testConfigName);
-    }
-
-    @Test
-    public void testMatchingForManyToManyEntity() throws Exception {
-        ObjEntity manyToManyEntity = map.getObjEntity("Table1Table2");
-
-        assertNotNull(ManyToManyCandidateEntity.build(manyToManyEntity));
-    }
-
-    @Test
-    public void testMatchingForNotManyToManyEntity() throws Exception {
-        ObjEntity entity = map.getObjEntity("Table1");
-
-        assertNull(ManyToManyCandidateEntity.build(entity));
-    }
-
-    @Test
-    public void testOptimisationForManyToManyEntity() {
-        ObjEntity manyToManyEntity = map.getObjEntity("Table1Table2");
-
-        ManyToManyCandidateEntity.build(manyToManyEntity).optimizeRelationships(new LegacyNameGenerator());
-
-        ObjEntity table1Entity = map.getObjEntity("Table1");
-        ObjEntity table2Entity = map.getObjEntity("Table2");
-
-        assertEquals(1, table1Entity.getRelationships().size());
-        assertEquals(table2Entity, new ArrayList<Relationship>(table1Entity.getRelationships()).get(0)
-                .getTargetEntity());
-
-        assertEquals(1, table2Entity.getRelationships().size());
-        assertEquals(table1Entity, new ArrayList<Relationship>(table2Entity.getRelationships()).get(0)
-                .getTargetEntity());
-    }
-
-}