cayenne-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From aadamc...@apache.org
Subject [11/15] cayenne git commit: CAY-2116 Split schema synchronization code in a separate module
Date Thu, 29 Sep 2016 17:38:57 GMT
http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/filters/IncludeTableFilter.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/filters/IncludeTableFilter.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/filters/IncludeTableFilter.java
new file mode 100644
index 0000000..ad68b49
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/filters/IncludeTableFilter.java
@@ -0,0 +1,71 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+package org.apache.cayenne.dbsync.reverse.filters;
+
+import java.util.regex.Pattern;
+
+/**
+* @since 4.0.
+*/
+public class IncludeTableFilter implements Comparable<IncludeTableFilter> {
+    public final Pattern pattern;
+
+    public final PatternFilter columnsFilter;
+
+    public IncludeTableFilter(String pattern) {
+        this(pattern, PatternFilter.INCLUDE_EVERYTHING);
+    }
+
+    public IncludeTableFilter(String pattern, PatternFilter columnsFilter) {
+        this.pattern = PatternFilter.pattern(pattern);
+        this.columnsFilter = columnsFilter;
+    }
+
+    public boolean isIncludeColumn (String name) {
+        return columnsFilter.isInclude(name);
+    }
+
+    @Override
+    public int compareTo(IncludeTableFilter o) {
+        if (pattern == null && o.pattern == null) {
+            return 0;
+        } else if (pattern == null) {
+            return 1;
+        } else if (o.pattern == null) {
+            return -1;
+        } else {
+            return pattern.pattern().compareTo(o.pattern.pattern());
+        }
+
+    }
+
+
+    @Override
+    public String toString() {
+        return toString(new StringBuilder(), "").toString();
+    }
+
+    protected StringBuilder toString(StringBuilder res, String prefix) {
+        res.append(prefix).append("Include: ").append(String.valueOf(pattern)).append(" Columns: ");
+        columnsFilter.toString(res);
+        res.append("\n");
+
+        return res;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/filters/LegacyFilterConfigBridge.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/filters/LegacyFilterConfigBridge.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/filters/LegacyFilterConfigBridge.java
new file mode 100644
index 0000000..b7e66fc
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/filters/LegacyFilterConfigBridge.java
@@ -0,0 +1,150 @@
+/*****************************************************************
+ *   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.filters;
+
+import static org.apache.commons.lang.StringUtils.isBlank;
+
+/**
+ * @since 4.0
+ */
+public class LegacyFilterConfigBridge {
+
+    private String catalog;
+    private String schema;
+
+    private String includeTableFilters;
+    private String includeColumnFilters;
+    private String includeProceduresFilters;
+    private String excludeTableFilters;
+    private String excludeColumnFilters;
+    private String excludeProceduresFilters;
+
+    private boolean loadProcedures;
+
+    public LegacyFilterConfigBridge() {
+    }
+
+    public LegacyFilterConfigBridge catalog(String catalog) {
+        this.catalog = catalog;
+        return this;
+    }
+
+    public String catalog() {
+        return catalog;
+    }
+
+    public LegacyFilterConfigBridge schema(String schema) {
+        this.schema = schema;
+        return this;
+    }
+
+    public String schema() {
+        return schema;
+    }
+
+    public LegacyFilterConfigBridge includeTables(String tableFilters) {
+        if (isBlank(tableFilters)) {
+            return this;
+        }
+
+        this.includeTableFilters = transform(tableFilters);
+        return this;
+    }
+
+    public LegacyFilterConfigBridge includeColumns(String columnFilters) {
+        if (isBlank(columnFilters)) {
+            return this;
+        }
+
+        this.includeColumnFilters = transform(columnFilters);
+        return this;
+    }
+
+    public LegacyFilterConfigBridge includeProcedures(String proceduresFilters) {
+        if (isBlank(proceduresFilters)) {
+            return this;
+        }
+
+        this.includeProceduresFilters = transform(proceduresFilters);
+        return this;
+    }
+
+    public LegacyFilterConfigBridge excludeTables(String tableFilters) {
+        if (isBlank(tableFilters)) {
+            return this;
+        }
+
+        this.excludeTableFilters = transform(tableFilters);
+        return this;
+    }
+
+    public LegacyFilterConfigBridge excludeColumns(String columnFilters) {
+        if (isBlank(columnFilters)) {
+            return this;
+        }
+
+        this.excludeColumnFilters = transform(columnFilters);
+        return this;
+    }
+
+    public LegacyFilterConfigBridge excludeProcedures(String proceduresFilters) {
+        if (isBlank(proceduresFilters)) {
+            return this;
+        }
+
+        this.excludeProceduresFilters = transform(proceduresFilters);
+        return this;
+    }
+
+    private static String transform(String pattern) {
+        return "^" + pattern.replaceAll("[*?]", ".$0") + "$";
+    }
+
+    public void setProceduresFilters(boolean loadProcedures) {
+        this.loadProcedures = loadProcedures;
+    }
+
+    public String getIncludeTableFilters() {
+        return includeTableFilters;
+    }
+
+    public String getIncludeColumnFilters() {
+        return includeColumnFilters;
+    }
+
+    public String getIncludeProceduresFilters() {
+        return includeProceduresFilters;
+    }
+
+    public String getExcludeTableFilters() {
+        return excludeTableFilters;
+    }
+
+    public String getExcludeColumnFilters() {
+        return excludeColumnFilters;
+    }
+
+    public String getExcludeProceduresFilters() {
+        return excludeProceduresFilters;
+    }
+
+    public boolean isLoadProcedures() {
+        return loadProcedures;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/filters/PatternFilter.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/filters/PatternFilter.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/filters/PatternFilter.java
new file mode 100644
index 0000000..4b0bf45
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/filters/PatternFilter.java
@@ -0,0 +1,169 @@
+/*****************************************************************
+ *   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.filters;
+
+import org.apache.commons.lang.StringUtils;
+
+import java.util.Comparator;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.regex.Pattern;
+
+/**
+ * @since 4.0
+ */
+public class PatternFilter {
+
+    public static final PatternFilter INCLUDE_EVERYTHING = new PatternFilter() {
+
+        @Override
+        public boolean isInclude(String obj) {
+            return true;
+        }
+
+        @Override
+        public StringBuilder toString(StringBuilder res) {
+            return res.append("ALL");
+        }
+    };
+
+    public static final PatternFilter INCLUDE_NOTHING = new PatternFilter() {
+        @Override
+        public boolean isInclude(String obj) {
+            return false;
+        }
+
+        @Override
+        public StringBuilder toString(StringBuilder res) {
+            return res.append("NONE");
+        }
+    };
+
+    public static final Comparator<Pattern> PATTERN_COMPARATOR = new Comparator<Pattern>() {
+        @Override
+        public int compare(Pattern o1, Pattern o2) {
+            if (o1 != null && o2 != null) {
+                return o1.pattern().compareTo(o2.pattern());
+            }
+            else {
+                return -1;
+            }
+        }
+    };
+
+    private final SortedSet<Pattern> includes;
+    private final SortedSet<Pattern> excludes;
+
+    public PatternFilter() {
+        this.includes = new TreeSet<>(PATTERN_COMPARATOR);
+        this.excludes = new TreeSet<>(PATTERN_COMPARATOR);
+    }
+
+    public PatternFilter include(Pattern p) {
+        includes.add(p);
+
+        return this;
+    }
+
+    public PatternFilter exclude(Pattern p) {
+        excludes.add(p);
+
+        return this;
+    }
+
+    public static Pattern pattern(String pattern) {
+        if (pattern == null) {
+            return null;
+        }
+        return Pattern.compile(pattern, Pattern.CASE_INSENSITIVE);
+    }
+
+    public PatternFilter include(String p) {
+        return include(pattern(p));
+    }
+
+    public PatternFilter exclude(String p) {
+        return exclude(pattern(p));
+    }
+
+    public boolean isInclude(String obj) {
+        boolean include = includes.isEmpty();
+        for (Pattern p : includes) {
+            if (p != null) {
+                if (p.matcher(obj).matches()) {
+                    include = true;
+                    break;
+                }
+            }
+        }
+
+        if (!include) {
+            return false;
+        }
+
+        for (Pattern p : excludes) {
+            if (p.matcher(obj).matches()) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+
+        PatternFilter filter = (PatternFilter) o;
+        return includes.equals(filter.includes)
+                && excludes.equals(filter.excludes);
+    }
+
+    @Override
+    public int hashCode() {
+        return includes.hashCode();
+    }
+
+    public StringBuilder toString(StringBuilder res) {
+        if (includes.isEmpty()) {
+            // Do nothing.
+        } else if (includes.size() > 1) {
+            res.append("(").append(StringUtils.join(includes, " OR ")).append(")");
+        } else {
+            res.append(includes.first().pattern());
+        }
+
+        if (!excludes.isEmpty()) {
+            res.append(" AND NOT (").append(StringUtils.join(includes, " OR ")).append(")");
+        }
+
+        return res;
+    }
+
+    public boolean isEmpty() {
+        return includes.isEmpty() && excludes.isEmpty();
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/filters/SchemaFilter.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/filters/SchemaFilter.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/filters/SchemaFilter.java
new file mode 100644
index 0000000..d486215
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/filters/SchemaFilter.java
@@ -0,0 +1,44 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+package org.apache.cayenne.dbsync.reverse.filters;
+
+/**
+* @since 4.0.
+*/
+public class SchemaFilter {
+    public final String name;
+    public final TableFilter tables;
+    public final PatternFilter procedures;
+
+    public SchemaFilter(String name, TableFilter tables, PatternFilter procedures) {
+        this.name = name;
+        this.tables = tables;
+        this.procedures = procedures;
+    }
+
+    protected StringBuilder toString(StringBuilder res, String prefix) {
+        res.append(prefix).append("Schema: ").append(name).append("\n");
+        tables.toString(res, prefix + "  ");
+
+        res.append(prefix).append("  Procedures: ");
+        procedures.toString(res).append("\n");
+
+        return res;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/filters/TableFilter.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/filters/TableFilter.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/filters/TableFilter.java
new file mode 100644
index 0000000..266bbc2
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/filters/TableFilter.java
@@ -0,0 +1,133 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+package org.apache.cayenne.dbsync.reverse.filters;
+
+import org.apache.commons.lang.StringUtils;
+
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.regex.Pattern;
+
+/**
+ * TableFilter contain at least one IncludeTable always
+ *
+ */
+public class TableFilter {
+
+    private final SortedSet<IncludeTableFilter> includes;
+    private final SortedSet<Pattern> excludes;
+
+    /**
+     * Includes can contain only One includetable
+     *
+     * @param includes
+     * @param excludes
+     */
+    public TableFilter(SortedSet<IncludeTableFilter> includes, SortedSet<Pattern> excludes) {
+        if (includes.isEmpty()) {
+            throw new IllegalArgumentException("TableFilter should contain at least one IncludeTableFilter always " +
+                    "and it is builder responsibility. If you need table filter without includes, use EmptyTableFilter");
+        }
+
+        this.includes = includes;
+        this.excludes = excludes;
+    }
+
+    /**
+     * Return filter for columns in case we should take this table
+     *
+     * @param tableName
+     * @return
+     */
+    public PatternFilter isIncludeTable(String tableName) {
+        IncludeTableFilter include = null;
+        for (IncludeTableFilter p : includes) {
+            if (p.pattern == null || p.pattern.matcher(tableName).matches()) {
+                include = p;
+                break;
+            }
+        }
+
+        if (include == null) {
+            return null;
+        }
+
+        for (Pattern p : excludes) {
+            if (p != null) {
+                if (p.matcher(tableName).matches()) {
+                    return null;
+                }
+            }
+        }
+
+        return include.columnsFilter;
+    }
+
+    public static TableFilter include(String tablePattern) {
+        TreeSet<IncludeTableFilter> includes = new TreeSet<IncludeTableFilter>();
+        includes.add(new IncludeTableFilter(tablePattern == null ? null : tablePattern.replaceAll("%", ".*")));
+
+        return new TableFilter(includes, new TreeSet<Pattern>());
+    }
+
+    public static TableFilter everything() {
+        TreeSet<IncludeTableFilter> includes = new TreeSet<IncludeTableFilter>();
+        includes.add(new IncludeTableFilter(null));
+
+        return new TableFilter(includes, new TreeSet<Pattern>());
+    }
+
+    protected StringBuilder toString(StringBuilder res, String prefix) {
+        res.append(prefix).append("Tables: ").append("\n");
+
+        for (IncludeTableFilter include : includes) {
+            include.toString(res, prefix + "  ");
+        }
+
+        if (!excludes.isEmpty()) {
+            res.append(prefix).append("  ").append(StringUtils.join(excludes, " OR ")).append("\n");
+        }
+
+        return res;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+
+        if (!(o instanceof TableFilter)) {
+            return false;
+        }
+
+        TableFilter that = (TableFilter) o;
+
+        return excludes.equals(that.excludes)
+                && includes.equals(that.includes);
+
+    }
+
+    @Override
+    public int hashCode() {
+        int result = includes.hashCode();
+        result = 31 * result + excludes.hashCode();
+        return result;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/mapper/DbType.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/mapper/DbType.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/mapper/DbType.java
new file mode 100644
index 0000000..9f829ea
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/mapper/DbType.java
@@ -0,0 +1,194 @@
+/*****************************************************************
+ *   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.mapper;
+
+import org.apache.cayenne.util.EqualsBuilder;
+import org.apache.cayenne.util.HashCodeBuilder;
+import org.apache.commons.lang.builder.CompareToBuilder;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import static org.apache.commons.lang.StringUtils.isBlank;
+
+/**
+ * @since 4.0
+ */
+public class DbType implements Comparable<DbType> {
+
+    private static final Log LOG = LogFactory.getLog(DbType.class);
+
+    public final String jdbc;
+
+    public final Integer length;
+    public final Integer precision;
+    public final Integer scale;
+    public final Boolean notNull;
+
+    public DbType(String jdbc) {
+        this(jdbc, null, null, null, null);
+    }
+
+    public DbType(String jdbc, Integer length, Integer precision, Integer scale, Boolean notNull) {
+        if (isBlank(jdbc)) {
+            throw new IllegalArgumentException("Jdbc type can't be null");
+        }
+        this.jdbc = jdbc;
+
+        this.length = getValidInt(length);
+        this.precision = getValidInt(precision);
+        this.scale = getValidInt(scale);
+        this.notNull = notNull;
+    }
+
+    public String getJdbc() {
+        return jdbc;
+    }
+
+    public Integer getLength() {
+        return length;
+    }
+
+    public Integer getPrecision() {
+        return precision;
+    }
+
+    public Integer getScale() {
+        return scale;
+    }
+
+    public Boolean getNotNull() {
+        return notNull;
+    }
+
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (obj == this) {
+            return true;
+        }
+        if (obj.getClass() != getClass()) {
+            return false;
+        }
+        DbType rhs = (DbType) obj;
+        return new EqualsBuilder()
+                .append(this.jdbc, rhs.jdbc)
+                .append(this.length, rhs.length)
+                .append(this.precision, rhs.precision)
+                .append(this.scale, rhs.scale)
+                .append(this.notNull, rhs.notNull)
+                .isEquals();
+    }
+
+    @Override
+    public int hashCode() {
+        return new HashCodeBuilder()
+                .append(jdbc)
+                .append(length)
+                .append(precision)
+                .append(scale)
+                .append(notNull)
+                .toHashCode();
+    }
+
+
+    @Override
+    public String toString() {
+        String res = jdbc;
+
+        String len = "*";
+        if (isPositive(length)) {
+            len = length.toString();
+        }
+        if (isPositive(precision)) {
+            len = precision.toString();
+        }
+
+        res += " (" + len;
+        if (isPositive(scale)) {
+            res += ", " + scale;
+        }
+        res += ")";
+
+        if (notNull != null && notNull) {
+            res += " NOT NULL";
+        }
+
+        return res;
+    }
+
+    private boolean isPositive(Integer num) {
+        return num != null && num > 0;
+    }
+
+    private Integer getValidInt(Integer num) {
+        if (num == null || num > 0) {
+            return num;
+        }
+
+        LOG.warn("Invalid int value '" + num + "'");
+        return null;
+    }
+
+    /**
+     * Compare by specificity the most specific DbPath should be first in ordered list
+     */
+    @Override
+    public int compareTo(DbType dbType) {
+        return new CompareToBuilder()
+                .append(dbType.jdbc, jdbc)
+                .append(dbType.getSpecificity(), getSpecificity())
+                .append(dbType.length, length)
+                .append(dbType.precision, precision)
+                .append(dbType.scale, scale)
+                .append(dbType.notNull, notNull)
+                .toComparison();
+    }
+
+    private int getSpecificity() {
+        int res = 0;
+        if (isPositive(length)) {
+            res += 100;
+        }
+        if (isPositive(precision)) {
+            res += 100;
+        }
+        if (isPositive(scale)) {
+            res += 10;
+        }
+        if (this.notNull != null) {
+            res += 5;
+        }
+
+        return res;
+    }
+
+    public boolean isCover(DbType type) {
+        return this.jdbc.equals(type.jdbc)
+            && (isCover(length, type.length) || length == null && type.length == null && isCover(precision, type.precision))
+            && isCover(scale, type.scale)
+            && isCover(notNull, type.notNull);
+    }
+
+    private boolean isCover(Object a, Object b) {
+        return a == null || a.equals(b);
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/mapper/DefaultJdbc2JavaTypeMapper.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/mapper/DefaultJdbc2JavaTypeMapper.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/mapper/DefaultJdbc2JavaTypeMapper.java
new file mode 100644
index 0000000..0af1690
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/mapper/DefaultJdbc2JavaTypeMapper.java
@@ -0,0 +1,282 @@
+/*****************************************************************
+ *   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.mapper;
+
+import org.apache.cayenne.dba.TypesMapping;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.util.Util;
+
+import java.io.Serializable;
+import java.math.BigInteger;
+import java.sql.Types;
+import java.util.Calendar;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+/**
+ * @since 4.0.
+ */
+public class DefaultJdbc2JavaTypeMapper implements Jdbc2JavaTypeMapper {
+
+	// Never use "-1" or any other normal integer, since there
+	// is a big chance it is being reserved in java.sql.Types
+	public static final int NOT_DEFINED = Integer.MAX_VALUE;
+
+	// char constants for Java data types
+	public static final String JAVA_LONG = "java.lang.Long";
+	public static final String JAVA_BYTES = "byte[]";
+	public static final String JAVA_BOOLEAN = "java.lang.Boolean";
+	public static final String JAVA_STRING = "java.lang.String";
+	public static final String JAVA_SQLDATE = "java.sql.Date";
+	public static final String JAVA_UTILDATE = "java.util.Date";
+	public static final String JAVA_BIGDECIMAL = "java.math.BigDecimal";
+	public static final String JAVA_DOUBLE = "java.lang.Double";
+	public static final String JAVA_FLOAT = "java.lang.Float";
+	public static final String JAVA_INTEGER = "java.lang.Integer";
+	public static final String JAVA_SHORT = "java.lang.Short";
+	public static final String JAVA_BYTE = "java.lang.Byte";
+	public static final String JAVA_TIME = "java.sql.Time";
+	public static final String JAVA_TIMESTAMP = "java.sql.Timestamp";
+	public static final String JAVA_BLOB = "java.sql.Blob";
+
+	/**
+	 * Keys: java class names, Values: SQL int type definitions from
+	 * java.sql.Types
+	 */
+	private final Map<String, Integer> javaSqlEnum = new HashMap<>();
+
+	private final Map<DbType, String> mapping = new HashMap<>();
+	private final SortedSet<DbType> dbTypes = new TreeSet<>();
+
+	private Map<String, String> classToPrimitive;
+
+	{
+		add(Types.BIGINT, JAVA_LONG);
+		add(Types.BINARY, JAVA_BYTES);
+		add(Types.BIT, JAVA_BOOLEAN);
+		add(Types.BOOLEAN, JAVA_BOOLEAN);
+		add(Types.BLOB, JAVA_BYTES);
+		add(Types.CLOB, JAVA_STRING);
+		add(Types.NCLOB, JAVA_STRING);
+		add(Types.SQLXML, JAVA_STRING);
+		add(Types.CHAR, JAVA_STRING);
+		add(Types.NCHAR, JAVA_STRING);
+		add(Types.DATE, JAVA_UTILDATE);
+		add(Types.DECIMAL, JAVA_BIGDECIMAL);
+		add(Types.DOUBLE, JAVA_DOUBLE);
+		add(Types.FLOAT, JAVA_FLOAT);
+		add(Types.INTEGER, JAVA_INTEGER);
+		add(Types.LONGVARCHAR, JAVA_STRING);
+		add(Types.LONGNVARCHAR, JAVA_STRING);
+		add(Types.LONGVARBINARY, JAVA_BYTES);
+		add(Types.NUMERIC, JAVA_BIGDECIMAL);
+		add(Types.REAL, JAVA_FLOAT);
+		add(Types.SMALLINT, JAVA_SHORT);
+		add(Types.TINYINT, JAVA_SHORT);
+		add(Types.TIME, JAVA_UTILDATE);
+		add(Types.TIMESTAMP, JAVA_UTILDATE);
+		add(Types.VARBINARY, JAVA_BYTES);
+		add(Types.VARCHAR, JAVA_STRING);
+		add(Types.NVARCHAR, JAVA_STRING);
+
+		javaSqlEnum.put(JAVA_LONG, Types.BIGINT);
+		javaSqlEnum.put(JAVA_BYTES, Types.BINARY);
+		javaSqlEnum.put(JAVA_BOOLEAN, Types.BIT);
+		javaSqlEnum.put(JAVA_STRING, Types.VARCHAR);
+		javaSqlEnum.put(JAVA_SQLDATE, Types.DATE);
+		javaSqlEnum.put(JAVA_UTILDATE, Types.DATE);
+		javaSqlEnum.put(JAVA_TIMESTAMP, Types.TIMESTAMP);
+		javaSqlEnum.put(JAVA_BIGDECIMAL, Types.DECIMAL);
+		javaSqlEnum.put(JAVA_DOUBLE, Types.DOUBLE);
+		javaSqlEnum.put(JAVA_FLOAT, Types.FLOAT);
+		javaSqlEnum.put(JAVA_INTEGER, Types.INTEGER);
+		javaSqlEnum.put(JAVA_SHORT, Types.SMALLINT);
+		javaSqlEnum.put(JAVA_BYTE, Types.SMALLINT);
+		javaSqlEnum.put(JAVA_TIME, Types.TIME);
+		javaSqlEnum.put(JAVA_TIMESTAMP, Types.TIMESTAMP);
+
+		// add primitives
+		javaSqlEnum.put("byte", Types.TINYINT);
+		javaSqlEnum.put("int", Types.INTEGER);
+		javaSqlEnum.put("short", Types.SMALLINT);
+		javaSqlEnum.put("char", Types.CHAR);
+		javaSqlEnum.put("double", Types.DOUBLE);
+		javaSqlEnum.put("long", Types.BIGINT);
+		javaSqlEnum.put("float", Types.FLOAT);
+		javaSqlEnum.put("boolean", Types.BIT);
+
+		classToPrimitive = new HashMap<>();
+		classToPrimitive.put(Byte.class.getName(), "byte");
+		classToPrimitive.put(Long.class.getName(), "long");
+		classToPrimitive.put(Double.class.getName(), "double");
+		classToPrimitive.put(Boolean.class.getName(), "boolean");
+		classToPrimitive.put(Float.class.getName(), "float");
+		classToPrimitive.put(Short.class.getName(), "short");
+		classToPrimitive.put(Integer.class.getName(), "int");
+	}
+
+	private Boolean usePrimitives;
+
+	/**
+	 * Returns default java.sql.Types type by the Java type name.
+	 *
+	 * @param className
+	 *            Fully qualified Java Class name.
+	 * @return The SQL type or NOT_DEFINED if no type found.
+	 */
+	public int getJdbcTypeByJava(DbAttribute attribute, String className) {
+		if (className == null) {
+			return NOT_DEFINED;
+		}
+
+		Integer type = javaSqlEnum.get(className);
+		if (type != null) {
+			return type;
+		}
+
+		// try to load a Java class - some nonstandard mappings may work
+		try {
+			return getSqlTypeByJava(attribute, Util.getJavaClass(className));
+		} catch (Throwable th) {
+			return NOT_DEFINED;
+		}
+	}
+
+	public void add(int jdbcType, String java) {
+		add(new DbType(TypesMapping.getSqlNameByType(jdbcType)), java);
+	}
+
+	@Override
+	public void add(DbType type, String java) {
+		mapping.put(type, java);
+		dbTypes.add(type);
+	}
+
+	/**
+	 * Guesses a default JDBC type for the Java class.
+	 *
+	 * @since 1.1
+	 */
+	protected int getSqlTypeByJava(DbAttribute attribute, Class<?> javaClass) {
+		if (javaClass == null) {
+			return NOT_DEFINED;
+		}
+
+		// check standard mapping of class and superclasses
+		Class<?> aClass = javaClass;
+		while (aClass != null) {
+
+			String name;
+
+			if (aClass.isArray()) {
+				name = aClass.getComponentType().getName() + "[]";
+			} else {
+				name = aClass.getName();
+			}
+
+			Object type = javaSqlEnum.get(name);
+			if (type != null) {
+				return ((Number) type).intValue();
+			}
+
+			aClass = aClass.getSuperclass();
+		}
+
+		// check non-standard JDBC types that are still supported by JPA
+		if (javaClass.isArray()) {
+
+			Class<?> elementType = javaClass.getComponentType();
+			if (Character.class.isAssignableFrom(elementType) || Character.TYPE.isAssignableFrom(elementType)) {
+				return Types.VARCHAR;
+			} else if (Byte.class.isAssignableFrom(elementType) || Byte.TYPE.isAssignableFrom(elementType)) {
+				return Types.VARBINARY;
+			}
+		}
+
+		if (Calendar.class.isAssignableFrom(javaClass)) {
+			return Types.TIMESTAMP;
+		}
+
+		if (BigInteger.class.isAssignableFrom(javaClass)) {
+			return Types.BIGINT;
+		}
+
+		if (Serializable.class.isAssignableFrom(javaClass)) {
+			// serializable check should be the last one when all other mapping
+			// attempts failed
+			return Types.VARBINARY;
+		}
+
+		return NOT_DEFINED;
+	}
+
+	/**
+	 * Get the corresponding Java type by its java.sql.Types counterpart. Note
+	 * that this method should be used as a last resort, with explicit mapping
+	 * provided by user used as a first choice, as it can only guess how to map
+	 * certain types, such as NUMERIC, etc.
+	 *
+	 * @return Fully qualified Java type name or null if not found.
+	 */
+	@Override
+	public String getJavaByJdbcType(DbAttribute attribute, int type) {
+		String jdbcType = TypesMapping.getSqlNameByType(type);
+		DbType dbType;
+		if (attribute != null) {
+			dbType = new DbType(jdbcType, attribute.getMaxLength(), attribute.getAttributePrecision(),
+					attribute.getScale(), attribute.isMandatory());
+		} else {
+			dbType = new DbType(jdbcType);
+		}
+
+		String typeName = getJavaByJdbcType(dbType);
+
+		if (usePrimitives != null && usePrimitives) {
+			String primitive = classToPrimitive.get(typeName);
+			if (primitive != null) {
+				return primitive;
+			}
+		}
+
+		return typeName;
+	}
+
+	public String getJavaByJdbcType(DbType type) {
+		for (DbType t : dbTypes) {
+			if (t.isCover(type)) {
+				// because dbTypes sorted by specificity we will take first and
+				// the most specific matching
+				// that applicable for attribute
+				return mapping.get(t);
+			}
+		}
+
+		return null;
+	}
+
+	public Boolean getUsePrimitives() {
+		return usePrimitives;
+	}
+
+	public void setUsePrimitives(Boolean usePrimitives) {
+		this.usePrimitives = usePrimitives;
+	}
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/mapper/Jdbc2JavaTypeMapper.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/mapper/Jdbc2JavaTypeMapper.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/mapper/Jdbc2JavaTypeMapper.java
new file mode 100644
index 0000000..71d86d5
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/mapper/Jdbc2JavaTypeMapper.java
@@ -0,0 +1,33 @@
+/*****************************************************************
+ *   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.mapper;
+
+import org.apache.cayenne.map.DbAttribute;
+
+/**
+ * @since 4.0.
+ */
+public interface Jdbc2JavaTypeMapper {
+
+    String getJavaByJdbcType(DbAttribute attribute, int type);
+
+    int getJdbcTypeByJava(DbAttribute attribute, String className);
+
+    void add(DbType type, String java);
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/AddColumnToModelIT.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/AddColumnToModelIT.java b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/AddColumnToModelIT.java
new file mode 100644
index 0000000..30fa47b
--- /dev/null
+++ b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/AddColumnToModelIT.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;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.sql.Types;
+import java.util.List;
+
+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.junit.Test;
+
+public class AddColumnToModelIT extends MergeCase {
+
+    @Test
+    public void testAddColumn() 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);
+
+        // remove name column
+        objEntity.removeAttribute(oatr1.getName());
+        dbEntity.removeAttribute(column2.getName());
+        assertNull(objEntity.getAttribute(oatr1.getName()));
+        assertEquals(0, objEntity.getAttributes().size());
+        assertNull(dbEntity.getAttribute(column2.getName()));
+
+        List<MergerToken> tokens = createMergeTokens();
+        assertEquals(1, tokens.size());
+        MergerToken token = tokens.get(0);
+        if (token.getDirection().isToDb()) {
+            token = token.createReverse(mergerFactory());
+        }
+        assertTrue(token instanceof AddColumnToModel);
+        execute(token);
+        assertEquals(1, objEntity.getAttributes().size());
+        assertEquals("java.lang.String", objEntity.getAttributes().iterator()
+                .next().getType());
+
+        // 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);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/CreateTableToModelIT.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/CreateTableToModelIT.java b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/CreateTableToModelIT.java
new file mode 100644
index 0000000..bac6402
--- /dev/null
+++ b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/CreateTableToModelIT.java
@@ -0,0 +1,97 @@
+/*****************************************************************
+ *   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.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.sql.Types;
+import java.util.List;
+
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.ObjEntity;
+import org.junit.Test;
+
+public class CreateTableToModelIT extends MergeCase {
+
+	@Test
+	public void testAddTable() 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);
+
+		// for the new entity to the db
+		execute(mergerFactory().createCreateTableToDb(dbEntity));
+
+		List<MergerToken> tokens = createMergeTokens();
+		assertEquals(1, tokens.size());
+		MergerToken token = tokens.get(0);
+		if (token.getDirection().isToDb()) {
+			token = token.createReverse(mergerFactory());
+		}
+		assertTrue(token.getClass().getName(), token instanceof CreateTableToModel);
+
+		execute(token);
+
+		ObjEntity objEntity = null;
+		for (ObjEntity candidate : map.getObjEntities()) {
+			if (dbEntity.getName().equalsIgnoreCase(candidate.getDbEntityName())) {
+				objEntity = candidate;
+				break;
+			}
+		}
+		assertNotNull(objEntity);
+
+		assertEquals(objEntity.getClassName(), map.getDefaultPackage() + "." + objEntity.getName());
+		assertEquals(objEntity.getSuperClassName(), map.getDefaultSuperclass());
+		assertEquals(objEntity.getClientClassName(), map.getDefaultClientPackage() + "." + objEntity.getName());
+		assertEquals(objEntity.getClientSuperClassName(), map.getDefaultClientSuperclass());
+
+		assertEquals(1, objEntity.getAttributes().size());
+		assertEquals("java.lang.String", objEntity.getAttributes().iterator().next().getType());
+
+		// clear up
+		// fix psql case issue
+		map.removeDbEntity(objEntity.getDbEntity().getName(), true);
+		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);
+	}
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/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
new file mode 100644
index 0000000..8cb03dd
--- /dev/null
+++ b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/DbMergerTest.java
@@ -0,0 +1,261 @@
+/*****************************************************************
+ *   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.builders.DbEntityBuilder;
+import org.apache.cayenne.dbsync.merge.factory.HSQLMergerTokenFactory;
+import org.apache.cayenne.dbsync.reverse.DbLoaderConfiguration;
+import org.apache.cayenne.map.DataMap;
+import org.apache.cayenne.map.DbEntity;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+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;
+
+public class DbMergerTest {
+
+    @Test
+    public void testEmptyDataMap() throws Exception {
+        assertEquals(0, dbMerger().createMergeTokens(new ArrayList<DbEntity>(0),
+                new ArrayList<DbEntity>(0), new DbLoaderConfiguration()).size());
+    }
+
+    @Test
+    public void testAddTable() throws Exception {
+        DbEntityBuilder dbEntity =
+            dbEntity("table1").attributes(
+                dbAttr("attr01").typeInt()
+        );
+        DataMap existing = dataMap().with(dbEntity).build();
+
+        List<MergerToken> tokens = dbMerger().createMergeTokens(existing.getDbEntities(),
+                new ArrayList<DbEntity>(0), new DbLoaderConfiguration());
+
+        assertEquals(1, tokens.size());
+        assertEquals(factory().createCreateTableToDb(dbEntity.build()).getTokenValue(),
+                     tokens.get(0).getTokenValue());
+    }
+
+    @Test
+    public void testRemoveTable() throws Exception {
+        DataMap db = dataMap().with(
+            dbEntity("table1").attributes(
+                dbAttr("attr01").typeInt()
+        )).build();
+
+        List<MergerToken> tokens = dbMerger().createMergeTokens(new ArrayList<DbEntity>(0),
+                db.getDbEntities(), new DbLoaderConfiguration());
+
+        assertEquals(1, tokens.size());
+        assertEquals(factory().createDropTableToDb(db.getDbEntity("table1")).getTokenValue(),
+                     tokens.get(0).getTokenValue());
+    }
+
+    @Test
+    public void testAddColumn() throws Exception {
+        DataMap existing = dataMap().with(
+            dbEntity("table1").attributes(
+                dbAttr("attr01").typeInt(),
+                dbAttr("attr02").typeInt()
+        )).build();
+
+        DataMap db = dataMap().with(
+            dbEntity("table1").attributes(
+                dbAttr("attr01").typeInt()
+        )).build();
+
+        List<MergerToken> tokens = dbMerger().createMergeTokens(existing.getDbEntities(),
+                db.getDbEntities(), new DbLoaderConfiguration());
+
+        assertEquals(1, tokens.size());
+
+        DbEntity entity = existing.getDbEntity("table1");
+        assertEquals(factory().createAddColumnToDb(entity, entity.getAttribute("attr02")).getTokenValue(),
+                     tokens.get(0).getTokenValue());
+    }
+
+    @Test
+    public void testAddRelationship() throws Exception {
+        DataMap existing = dataMap().with(
+            dbEntity("table1").attributes(
+                dbAttr("attr01").typeInt(),
+                dbAttr("attr02").typeInt()),
+
+            dbEntity("table2").attributes(
+                dbAttr("attr01").typeInt().primaryKey(),
+                dbAttr("attr02").typeInt())
+        ).join("rel", "table1.attr01", "table2.attr01")
+         .build();
+
+        DataMap db = dataMap().with(
+            dbEntity("table1").attributes(
+                dbAttr("attr01").typeInt(),
+                dbAttr("attr02").typeInt()),
+
+            dbEntity("table2").attributes(
+                dbAttr("attr01").typeInt().primaryKey(),
+                dbAttr("attr02").typeInt())
+        )//.join("table1.attr01", "table2.attr01")
+         .build();
+
+
+        List<MergerToken> tokens = dbMerger().createMergeTokens(existing.getDbEntities(),
+                db.getDbEntities(), new DbLoaderConfiguration());
+
+        assertEquals(1, tokens.size());
+
+        DbEntity entity = existing.getDbEntity("table1");
+        assertEquals(factory().createAddRelationshipToDb(entity, entity.getRelationship("rel")).getTokenValue(),
+                     tokens.get(0).getTokenValue());
+    }
+
+    @Test
+    public void testAddRelationship1() throws Exception {
+        DataMap existing = dataMap().with(
+            dbEntity("table1").attributes(
+                dbAttr("attr01").typeInt(),
+                dbAttr("attr02").typeInt()),
+
+            dbEntity("table2").attributes(
+                dbAttr("attr01").typeInt().primaryKey(),
+                dbAttr("attr02").typeInt().primaryKey(),
+                dbAttr("attr03").typeInt().primaryKey())
+        ).join("rel", "table1.attr01", "table2.attr01")
+         .join("rel1", "table1.attr01", "table2.attr03")
+         .build();
+
+        DataMap db = dataMap().with(
+            dbEntity("table1").attributes(
+                dbAttr("attr01").typeInt(),
+                dbAttr("attr02").typeInt()),
+
+            dbEntity("table2").attributes(
+                dbAttr("attr01").typeInt().primaryKey(),
+                dbAttr("attr02").typeInt().primaryKey(),
+                dbAttr("attr03").typeInt().primaryKey())
+        ).join("rel", "table1.attr01", "table2.attr02")
+         .join("rel1", "table1.attr01", "table2.attr03")
+         .build();
+
+
+        List<MergerToken> tokens = dbMerger().createMergeTokens(existing.getDbEntities(),
+                db.getDbEntities(), new DbLoaderConfiguration());
+
+        assertEquals(2, tokens.size());
+
+        DbEntity entity = existing.getDbEntity("table1");
+        assertEquals(factory().createDropRelationshipToDb(entity, entity.getRelationship("rel")).getTokenValue(),
+                     tokens.get(0).getTokenValue());
+
+        entity = db.getDbEntity("table1");
+        assertEquals(factory().createAddRelationshipToDb(entity, entity.getRelationship("rel")).getTokenValue(),
+                     tokens.get(0).getTokenValue());
+    }
+
+    @Test
+    public void testRemoveRelationship() throws Exception {
+        DataMap existing = dataMap().with(
+            dbEntity("table1").attributes(
+                dbAttr("attr01").typeInt(),
+                dbAttr("attr02").typeInt()),
+
+            dbEntity("table2").attributes(
+                dbAttr("attr01").typeInt().primaryKey(),
+                dbAttr("attr02").typeInt())
+        )
+         .build();
+
+        DataMap db = dataMap().with(
+            dbEntity("table1").attributes(
+                dbAttr("attr01").typeInt(),
+                dbAttr("attr02").typeInt()),
+
+            dbEntity("table2").attributes(
+                dbAttr("attr01").typeInt().primaryKey(),
+                dbAttr("attr02").typeInt())
+        ).join("rel", "table1.attr01", "table2.attr01")
+         .build();
+
+
+        List<MergerToken> tokens = dbMerger().createMergeTokens(existing.getDbEntities(), db.getDbEntities(), new DbLoaderConfiguration());
+
+        assertEquals(1, tokens.size());
+
+        DbEntity entity = db.getDbEntity("table1");
+        assertEquals(factory().createDropRelationshipToDb(entity, entity.getRelationship("rel")).getTokenValue(),
+                     tokens.get(0).getTokenValue());
+    }
+
+    @Test
+    public void testRemoveColumn() throws Exception {
+        DataMap existing = dataMap().with(
+            dbEntity("table1").attributes(
+                dbAttr("attr01").typeInt()
+        )).build();
+
+        DataMap db = dataMap().with(
+            dbEntity("table1").attributes(
+                dbAttr("attr01").typeInt(),
+                dbAttr("attr02").typeInt()
+        )).build();
+
+        List<MergerToken> tokens = dbMerger().createMergeTokens(existing.getDbEntities(),
+                db.getDbEntities(), new DbLoaderConfiguration());
+
+        assertEquals(1, tokens.size());
+
+        DbEntity entity = db.getDbEntity("table1");
+        assertEquals(factory().createDropColumnToModel(entity, entity.getAttribute("attr02")).getTokenValue(),
+                     tokens.get(0).getTokenValue());
+    }
+
+    @Test
+    public void testNoChanges() throws Exception {
+        DataMap dataMap1 = dataMap().with(
+                dbEntity("table1").attributes(
+                        dbAttr("attr01").typeInt(),
+                        dbAttr("attr02").typeInt(),
+                        dbAttr("attr03").typeInt()
+                )).build();
+
+        DataMap dataMap2 = dataMap().with(
+            dbEntity("table1").attributes(
+                dbAttr("attr01").typeInt(),
+                dbAttr("attr02").typeInt(),
+                dbAttr("attr03").typeInt()
+        )).build();
+
+
+        assertEquals(0, dbMerger().createMergeTokens(dataMap1.getDbEntities(),
+                dataMap2.getDbEntities(), new DbLoaderConfiguration()).size());
+    }
+
+    private DbMerger dbMerger() {
+        return new DbMerger(factory());
+    }
+
+    private HSQLMergerTokenFactory factory() {
+        return new HSQLMergerTokenFactory();
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/DropColumnToModelIT.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/DropColumnToModelIT.java b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/DropColumnToModelIT.java
new file mode 100644
index 0000000..f6e0675
--- /dev/null
+++ b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/DropColumnToModelIT.java
@@ -0,0 +1,235 @@
+/*****************************************************************
+ *   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.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.sql.Types;
+import java.util.List;
+
+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.apache.cayenne.map.ObjRelationship;
+import org.junit.Test;
+
+public class DropColumnToModelIT extends MergeCase {
+
+	@Test
+	public void testSimpleColumn() 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);
+
+		// force drop name column in db
+		MergerToken token = mergerFactory().createDropColumnToDb(dbEntity, column2);
+		execute(token);
+
+		List<MergerToken> tokens = createMergeTokens();
+		assertEquals(1, tokens.size());
+		token = tokens.get(0);
+		if (token.getDirection().isToDb()) {
+			token = token.createReverse(mergerFactory());
+		}
+		assertTrue(token instanceof DropColumnToModel);
+		execute(token);
+		assertNull(dbEntity.getAttribute(column2.getName()));
+		assertNull(objEntity.getAttribute(oatr1.getName()));
+
+		// 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 testRemoveFKColumnWithoutRelationshipInDb() throws Exception {
+		dropTableIfPresent("NEW_TABLE");
+		dropTableIfPresent("NEW_TABLE2");
+
+		assertTokensAndExecute(0, 0);
+
+		DbEntity dbEntity1 = new DbEntity("NEW_TABLE");
+
+		DbAttribute e1col1 = new DbAttribute("ID", Types.INTEGER, dbEntity1);
+		e1col1.setMandatory(true);
+		e1col1.setPrimaryKey(true);
+		dbEntity1.addAttribute(e1col1);
+
+		DbAttribute e1col2 = new DbAttribute("NAME", Types.VARCHAR, dbEntity1);
+		e1col2.setMaxLength(10);
+		e1col2.setMandatory(false);
+		dbEntity1.addAttribute(e1col2);
+
+		map.addDbEntity(dbEntity1);
+
+		DbEntity dbEntity2 = new DbEntity("NEW_TABLE2");
+		DbAttribute e2col1 = new DbAttribute("ID", Types.INTEGER, dbEntity2);
+		e2col1.setMandatory(true);
+		e2col1.setPrimaryKey(true);
+		dbEntity2.addAttribute(e2col1);
+		DbAttribute e2col2 = new DbAttribute("FK", Types.INTEGER, dbEntity2);
+		dbEntity2.addAttribute(e2col2);
+		DbAttribute e2col3 = new DbAttribute("NAME", Types.VARCHAR, dbEntity2);
+		e2col3.setMaxLength(10);
+		dbEntity2.addAttribute(e2col3);
+
+		map.addDbEntity(dbEntity2);
+
+		assertTokensAndExecute(2, 0);
+		assertTokensAndExecute(0, 0);
+
+		// force drop fk column in db
+		execute(mergerFactory().createDropColumnToDb(dbEntity2, e2col2));
+
+		// create db relationships, but do not sync them to db
+		DbRelationship rel1To2 = new DbRelationship("rel1To2");
+		rel1To2.setSourceEntity(dbEntity1);
+		rel1To2.setTargetEntityName(dbEntity2);
+		rel1To2.setToMany(true);
+		rel1To2.addJoin(new DbJoin(rel1To2, e1col1.getName(), e2col2.getName()));
+		dbEntity1.addRelationship(rel1To2);
+		DbRelationship rel2To1 = new DbRelationship("rel2To1");
+		rel2To1.setSourceEntity(dbEntity2);
+		rel2To1.setTargetEntityName(dbEntity1);
+		rel2To1.setToMany(false);
+		rel2To1.addJoin(new DbJoin(rel2To1, e2col2.getName(), e1col1.getName()));
+		dbEntity2.addRelationship(rel2To1);
+		assertSame(rel1To2, rel2To1.getReverseRelationship());
+		assertSame(rel2To1, rel1To2.getReverseRelationship());
+
+		// create ObjEntities
+		ObjEntity objEntity1 = new ObjEntity("NewTable");
+		objEntity1.setDbEntity(dbEntity1);
+		ObjAttribute oatr1 = new ObjAttribute("name");
+		oatr1.setDbAttributePath(e1col2.getName());
+		oatr1.setType("java.lang.String");
+		objEntity1.addAttribute(oatr1);
+		map.addObjEntity(objEntity1);
+		ObjEntity objEntity2 = new ObjEntity("NewTable2");
+		objEntity2.setDbEntity(dbEntity2);
+		ObjAttribute o2a1 = new ObjAttribute("name");
+		o2a1.setDbAttributePath(e2col3.getName());
+		o2a1.setType("java.lang.String");
+		objEntity2.addAttribute(o2a1);
+		map.addObjEntity(objEntity2);
+
+		// create ObjRelationships
+		assertEquals(0, objEntity1.getRelationships().size());
+		assertEquals(0, objEntity2.getRelationships().size());
+		ObjRelationship objRel1To2 = new ObjRelationship("objRel1To2");
+		objRel1To2.addDbRelationship(rel1To2);
+		objRel1To2.setSourceEntity(objEntity1);
+		objRel1To2.setTargetEntityName(objEntity2);
+		objEntity1.addRelationship(objRel1To2);
+		ObjRelationship objRel2To1 = new ObjRelationship("objRel2To1");
+		objRel2To1.addDbRelationship(rel2To1);
+		objRel2To1.setSourceEntity(objEntity2);
+		objRel2To1.setTargetEntityName(objEntity1);
+		objEntity2.addRelationship(objRel2To1);
+		assertEquals(1, objEntity1.getRelationships().size());
+		assertEquals(1, objEntity2.getRelationships().size());
+		assertSame(objRel1To2, objRel2To1.getReverseRelationship());
+		assertSame(objRel2To1, objRel1To2.getReverseRelationship());
+
+		// try do use the merger to remove the column and relationship in the
+		// model
+		List<MergerToken> tokens = createMergeTokens();
+		assertTokens(tokens, 2, 0);
+		// TODO: reversing the following two tokens should also reverse the
+		// order
+		MergerToken token0 = tokens.get(0).createReverse(mergerFactory());
+		MergerToken token1 = tokens.get(1).createReverse(mergerFactory());
+		if (!(token0 instanceof DropRelationshipToModel && token1 instanceof DropColumnToModel || token1 instanceof DropRelationshipToModel
+				&& token0 instanceof DropColumnToModel)) {
+			fail();
+		}
+		// do not execute DropRelationshipToModel, only DropColumnToModel.
+		if (token1 instanceof DropColumnToModel) {
+			execute(token1);
+		} else {
+			execute(token0);
+		}
+
+		// check after merging
+		assertNull(dbEntity2.getAttribute(e2col2.getName()));
+		assertEquals(0, dbEntity1.getRelationships().size());
+		assertEquals(0, dbEntity2.getRelationships().size());
+		assertEquals(0, objEntity1.getRelationships().size());
+		assertEquals(0, objEntity2.getRelationships().size());
+
+		// clear up
+
+		dbEntity1.removeRelationship(rel1To2.getName());
+		dbEntity2.removeRelationship(rel2To1.getName());
+		map.removeObjEntity(objEntity1.getName(), true);
+		map.removeDbEntity(dbEntity1.getName(), true);
+		map.removeObjEntity(objEntity2.getName(), true);
+		map.removeDbEntity(dbEntity2.getName(), true);
+		resolver.refreshMappingCache();
+		assertNull(map.getObjEntity(objEntity1.getName()));
+		assertNull(map.getDbEntity(dbEntity1.getName()));
+		assertNull(map.getObjEntity(objEntity2.getName()));
+		assertNull(map.getDbEntity(dbEntity2.getName()));
+		assertFalse(map.getDbEntities().contains(dbEntity1));
+		assertFalse(map.getDbEntities().contains(dbEntity2));
+
+		assertTokensAndExecute(2, 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/DropRelationshipToModelIT.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/DropRelationshipToModelIT.java b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/DropRelationshipToModelIT.java
new file mode 100644
index 0000000..cf2ec3d
--- /dev/null
+++ b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/DropRelationshipToModelIT.java
@@ -0,0 +1,188 @@
+/*****************************************************************
+ *   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.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.fail;
+
+import java.sql.Types;
+import java.util.List;
+
+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.apache.cayenne.map.ObjRelationship;
+import org.junit.Test;
+
+public class DropRelationshipToModelIT extends MergeCase {
+
+	@Test
+	public void testForeignKey() throws Exception {
+		dropTableIfPresent("NEW_TABLE");
+		dropTableIfPresent("NEW_TABLE2");
+
+		assertTokensAndExecute(0, 0);
+
+		DbEntity dbEntity1 = new DbEntity("NEW_TABLE");
+
+		DbAttribute e1col1 = new DbAttribute("ID", Types.INTEGER, dbEntity1);
+		e1col1.setMandatory(true);
+		e1col1.setPrimaryKey(true);
+		dbEntity1.addAttribute(e1col1);
+
+		DbAttribute e1col2 = new DbAttribute("NAME", Types.VARCHAR, dbEntity1);
+		e1col2.setMaxLength(10);
+		e1col2.setMandatory(false);
+		dbEntity1.addAttribute(e1col2);
+
+		map.addDbEntity(dbEntity1);
+
+		DbEntity dbEntity2 = new DbEntity("NEW_TABLE2");
+		DbAttribute e2col1 = new DbAttribute("ID", Types.INTEGER, dbEntity2);
+		e2col1.setMandatory(true);
+		e2col1.setPrimaryKey(true);
+		dbEntity2.addAttribute(e2col1);
+		DbAttribute e2col2 = new DbAttribute("FK", Types.INTEGER, dbEntity2);
+		dbEntity2.addAttribute(e2col2);
+		DbAttribute e2col3 = new DbAttribute("NAME", Types.VARCHAR, dbEntity2);
+		e2col3.setMaxLength(10);
+		dbEntity2.addAttribute(e2col3);
+
+		map.addDbEntity(dbEntity2);
+
+		// create db relationships
+		DbRelationship rel1To2 = new DbRelationship("rel1To2");
+		rel1To2.setSourceEntity(dbEntity1);
+		rel1To2.setTargetEntityName(dbEntity2);
+		rel1To2.setToMany(true);
+		rel1To2.addJoin(new DbJoin(rel1To2, e1col1.getName(), e2col2.getName()));
+		dbEntity1.addRelationship(rel1To2);
+		DbRelationship rel2To1 = new DbRelationship("rel2To1");
+		rel2To1.setSourceEntity(dbEntity2);
+		rel2To1.setTargetEntityName(dbEntity1);
+		rel2To1.setToMany(false);
+		rel2To1.addJoin(new DbJoin(rel2To1, e2col2.getName(), e1col1.getName()));
+		dbEntity2.addRelationship(rel2To1);
+		assertSame(rel1To2, rel2To1.getReverseRelationship());
+		assertSame(rel2To1, rel1To2.getReverseRelationship());
+
+		assertTokensAndExecute(4, 0);
+		assertTokensAndExecute(0, 0);
+
+		// create ObjEntities
+		ObjEntity objEntity1 = new ObjEntity("NewTable");
+		objEntity1.setDbEntity(dbEntity1);
+		ObjAttribute oatr1 = new ObjAttribute("name");
+		oatr1.setDbAttributePath(e1col2.getName());
+		oatr1.setType("java.lang.String");
+		objEntity1.addAttribute(oatr1);
+		map.addObjEntity(objEntity1);
+		ObjEntity objEntity2 = new ObjEntity("NewTable2");
+		objEntity2.setDbEntity(dbEntity2);
+		ObjAttribute o2a1 = new ObjAttribute("name");
+		o2a1.setDbAttributePath(e2col3.getName());
+		o2a1.setType("java.lang.String");
+		objEntity2.addAttribute(o2a1);
+		map.addObjEntity(objEntity2);
+
+		// create ObjRelationships
+		assertEquals(0, objEntity1.getRelationships().size());
+		assertEquals(0, objEntity2.getRelationships().size());
+		ObjRelationship objRel1To2 = new ObjRelationship("objRel1To2");
+		objRel1To2.addDbRelationship(rel1To2);
+		objRel1To2.setSourceEntity(objEntity1);
+		objRel1To2.setTargetEntityName(objEntity2);
+		objEntity1.addRelationship(objRel1To2);
+		ObjRelationship objRel2To1 = new ObjRelationship("objRel2To1");
+		objRel2To1.addDbRelationship(rel2To1);
+		objRel2To1.setSourceEntity(objEntity2);
+		objRel2To1.setTargetEntityName(objEntity1);
+		objEntity2.addRelationship(objRel2To1);
+		assertEquals(1, objEntity1.getRelationships().size());
+		assertEquals(1, objEntity2.getRelationships().size());
+		assertSame(objRel1To2, objRel2To1.getReverseRelationship());
+		assertSame(objRel2To1, objRel1To2.getReverseRelationship());
+
+        // remove relationship and fk from model, merge to db and read to model
+        dbEntity2.removeRelationship(rel2To1.getName());
+        dbEntity1.removeRelationship(rel1To2.getName());
+        dbEntity2.removeAttribute(e2col2.getName());
+        List<MergerToken> tokens = createMergeTokens();
+        /**
+         * Add Relationship NEW_TABLE->NEW_TABLE2 To Model
+         * Drop Relationship NEW_TABLE2->NEW_TABLE To DB
+         * Drop Column NEW_TABLE2.FK To DB
+         * */
+        assertTokens(tokens, 2, 1);
+        for (MergerToken token : tokens) {
+            if (token.getDirection().isToDb()) {
+                execute(token);
+            }
+        }
+        assertTokensAndExecute(0, 0);
+        dbEntity2.addRelationship(rel2To1);
+        dbEntity1.addRelationship(rel1To2);
+        dbEntity2.addAttribute(e2col2);
+
+		// try do use the merger to remove the relationship in the model
+		tokens = createMergeTokens();
+		assertTokens(tokens, 2, 0);
+		// TODO: reversing the following two tokens should also reverse the
+		// order
+		MergerToken token0 = tokens.get(0).createReverse(mergerFactory());
+		MergerToken token1 = tokens.get(1).createReverse(mergerFactory());
+		if (!(token0 instanceof DropRelationshipToModel && token1 instanceof DropColumnToModel || token1 instanceof DropRelationshipToModel
+				&& token0 instanceof DropColumnToModel)) {
+			fail();
+		}
+		execute(token0);
+		execute(token1);
+
+		// check after merging
+		assertNull(dbEntity2.getAttribute(e2col2.getName()));
+		assertEquals(0, dbEntity1.getRelationships().size());
+		assertEquals(0, dbEntity2.getRelationships().size());
+		assertEquals(0, objEntity1.getRelationships().size());
+		assertEquals(0, objEntity2.getRelationships().size());
+
+		// clear up
+		dbEntity1.removeRelationship(rel1To2.getName());
+		dbEntity2.removeRelationship(rel2To1.getName());
+		map.removeObjEntity(objEntity1.getName(), true);
+		map.removeDbEntity(dbEntity1.getName(), true);
+		map.removeObjEntity(objEntity2.getName(), true);
+		map.removeDbEntity(dbEntity2.getName(), true);
+		resolver.refreshMappingCache();
+		assertNull(map.getObjEntity(objEntity1.getName()));
+		assertNull(map.getDbEntity(dbEntity1.getName()));
+		assertNull(map.getObjEntity(objEntity2.getName()));
+		assertNull(map.getDbEntity(dbEntity2.getName()));
+		assertFalse(map.getDbEntities().contains(dbEntity1));
+		assertFalse(map.getDbEntities().contains(dbEntity2));
+
+		assertTokensAndExecute(2, 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/DropTableToModelIT.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/DropTableToModelIT.java b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/DropTableToModelIT.java
new file mode 100644
index 0000000..5b1744a
--- /dev/null
+++ b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/DropTableToModelIT.java
@@ -0,0 +1,94 @@
+/*****************************************************************
+ *   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.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.sql.Types;
+import java.util.List;
+
+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.junit.Test;
+
+public class DropTableToModelIT extends MergeCase {
+
+	@Test
+	public void testDropTable() 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);
+
+		// force drop table in db
+		MergerToken token = mergerFactory().createDropTableToDb(dbEntity);
+		execute(token);
+
+		List<MergerToken> tokens = createMergeTokens();
+		assertEquals(1, tokens.size());
+		token = tokens.get(0);
+		if (token.getDirection().isToDb()) {
+			token = token.createReverse(mergerFactory());
+		}
+		assertTrue(token instanceof DropTableToModel);
+		execute(token);
+		resolver.refreshMappingCache();
+		assertNull(map.getDbEntity(dbEntity.getName()));
+		assertNull(map.getObjEntity(objEntity.getName()));
+
+		// 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(0, 0);
+	}
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/2f7b1d53/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/EntityMergeSupportIT.java
----------------------------------------------------------------------
diff --git a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/EntityMergeSupportIT.java b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/EntityMergeSupportIT.java
new file mode 100644
index 0000000..d63c6a4
--- /dev/null
+++ b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/merge/EntityMergeSupportIT.java
@@ -0,0 +1,102 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+package org.apache.cayenne.dbsync.merge;
+
+import org.apache.cayenne.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.DeleteRule;
+import org.apache.cayenne.map.ObjEntity;
+import org.junit.Test;
+
+import java.sql.Types;
+import java.util.Arrays;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+public class EntityMergeSupportIT extends MergeCase {
+
+	@Test
+	public void testMerging() {
+		DbEntity dbEntity1 = new DbEntity("NEW_TABLE");
+
+		DbAttribute e1col1 = new DbAttribute("ID", Types.INTEGER, dbEntity1);
+		e1col1.setMandatory(true);
+		e1col1.setPrimaryKey(true);
+		dbEntity1.addAttribute(e1col1);
+
+		DbAttribute e1col2 = new DbAttribute("NAME", Types.VARCHAR, dbEntity1);
+		e1col2.setMaxLength(10);
+		e1col2.setMandatory(false);
+		dbEntity1.addAttribute(e1col2);
+
+		map.addDbEntity(dbEntity1);
+
+		DbEntity dbEntity2 = new DbEntity("NEW_TABLE2");
+		DbAttribute e2col1 = new DbAttribute("ID", Types.INTEGER, dbEntity2);
+		e2col1.setMandatory(true);
+		e2col1.setPrimaryKey(true);
+		dbEntity2.addAttribute(e2col1);
+		DbAttribute e2col2 = new DbAttribute("FK", Types.INTEGER, dbEntity2);
+		dbEntity2.addAttribute(e2col2);
+
+		map.addDbEntity(dbEntity2);
+
+		// create db relationships
+		DbRelationship rel1To2 = new DbRelationship("rel1To2");
+		rel1To2.setSourceEntity(dbEntity1);
+		rel1To2.setTargetEntityName(dbEntity2);
+		rel1To2.setToMany(true);
+		rel1To2.addJoin(new DbJoin(rel1To2, e1col1.getName(), e2col2.getName()));
+		dbEntity1.addRelationship(rel1To2);
+		DbRelationship rel2To1 = new DbRelationship("rel2To1");
+		rel2To1.setSourceEntity(dbEntity2);
+		rel2To1.setTargetEntityName(dbEntity1);
+		rel2To1.setToMany(false);
+		rel2To1.addJoin(new DbJoin(rel2To1, e2col2.getName(), e1col1.getName()));
+		dbEntity2.addRelationship(rel2To1);
+		assertSame(rel1To2, rel2To1.getReverseRelationship());
+		assertSame(rel2To1, rel1To2.getReverseRelationship());
+
+		ObjEntity objEntity1 = new ObjEntity("NewTable");
+		objEntity1.setDbEntity(dbEntity1);
+		map.addObjEntity(objEntity1);
+
+		ObjEntity objEntity2 = new ObjEntity("NewTable2");
+		objEntity2.setDbEntity(dbEntity2);
+		map.addObjEntity(objEntity2);
+
+		assertTrue(new EntityMergeSupport(map).synchronizeWithDbEntities(Arrays.asList(objEntity1, objEntity2)));
+		assertNotNull(objEntity1.getAttribute("name"));
+		assertNotNull(objEntity1.getRelationship("rel1To2"));
+		assertNotNull(objEntity2.getRelationship("rel2To1"));
+
+		assertEquals(objEntity1.getRelationship("rel1To2").getDeleteRule(), DeleteRule.DEFAULT_DELETE_RULE_TO_MANY);
+		assertEquals(objEntity2.getRelationship("rel2To1").getDeleteRule(), DeleteRule.DEFAULT_DELETE_RULE_TO_ONE);
+
+		map.removeObjEntity(objEntity2.getName());
+		map.removeObjEntity(objEntity1.getName());
+		map.removeDbEntity(dbEntity2.getName());
+		map.removeDbEntity(dbEntity1.getName());
+	}
+}


Mime
View raw message