All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jooq.impl.Diff Maven / Gradle / Ivy

There is a newer version: 3.19.15
Show newest version
/*
 * Licensed 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
 *
 *  https://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.
 *
 * Other licenses:
 * -----------------------------------------------------------------------------
 * Commercial licenses for this work are available. These replace the above
 * Apache-2.0 and offer limited warranties, support, maintenance, and commercial
 * database integrations.
 *
 * For more information, please visit: https://www.jooq.org/legal/licensing
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */
package org.jooq.impl;

import static java.lang.Boolean.FALSE;
import static java.util.Arrays.asList;
// ...
import static org.jooq.SQLDialect.IGNITE;
import static org.jooq.SQLDialect.MARIADB;
// ...
import static org.jooq.SQLDialect.MYSQL;
import static org.jooq.impl.Comparators.CHECK_COMP;
import static org.jooq.impl.Comparators.FOREIGN_KEY_COMP;
import static org.jooq.impl.Comparators.INDEX_COMP;
import static org.jooq.impl.Comparators.KEY_COMP;
import static org.jooq.impl.Comparators.NAMED_COMP;
import static org.jooq.impl.ConstraintType.CHECK;
import static org.jooq.impl.ConstraintType.FOREIGN_KEY;
import static org.jooq.impl.ConstraintType.PRIMARY_KEY;
import static org.jooq.impl.ConstraintType.UNIQUE;
import static org.jooq.impl.Tools.NO_SUPPORT_TIMESTAMP_PRECISION;
import static org.jooq.impl.Tools.allMatch;
import static org.jooq.impl.Tools.anyMatch;
import static org.jooq.tools.StringUtils.defaultIfNull;
import static org.jooq.tools.StringUtils.defaultString;
import static org.jooq.tools.StringUtils.isEmpty;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.jooq.AlterSequenceFlagsStep;
import org.jooq.Catalog;
import org.jooq.Check;
import org.jooq.Configuration;
import org.jooq.DDLExportConfiguration;
import org.jooq.DSLContext;
import org.jooq.DataType;
import org.jooq.Domain;
import org.jooq.Field;
import org.jooq.ForeignKey;
import org.jooq.Index;
import org.jooq.Key;
import org.jooq.Meta;
import org.jooq.MigrationConfiguration;
import org.jooq.Name;
import org.jooq.Named;
import org.jooq.Nullability;
import org.jooq.Queries;
import org.jooq.Query;
import org.jooq.SQLDialect;
import org.jooq.Schema;
import org.jooq.Sequence;
import org.jooq.Table;
import org.jooq.TableOptions.TableType;
import org.jooq.UniqueKey;
import org.jooq.tools.StringUtils;

/**
 * A class producing a diff between two {@link Meta} objects.
 *
 * @author Lukas Eder
 */
final class Diff {

    private static final Set NO_SUPPORT_PK_NAMES = SQLDialect.supportedBy(IGNITE, MARIADB, MYSQL);

    private final MigrationConfiguration migrateConf;
    private final DDLExportConfiguration exportConf;
    private final DSLContext             ctx;
    private final Meta                   meta1;
    private final Meta                   meta2;
    private final DDL                    ddl;

    Diff(Configuration configuration, MigrationConfiguration migrateConf, Meta meta1, Meta meta2) {
        this.migrateConf = migrateConf;
        this.exportConf = new DDLExportConfiguration().createOrReplaceView(migrateConf.createOrReplaceView());
        this.ctx = configuration.dsl();
        this.meta1 = meta1;
        this.meta2 = meta2;
        this.ddl = new DDL(ctx, exportConf);
    }

    final Queries queries() {
        return ctx.queries(appendCatalogs(new DiffResult(), meta1.getCatalogs(), meta2.getCatalogs()).queries);
    }

    private final DiffResult appendCatalogs(DiffResult result, List l1, List l2) {
        return append(result, l1, l2, null,

            // TODO Implement this for SQL Server support.
            null,

            // TODO Implement this for SQL Server support.
            null,

            (r, c1, c2) -> appendSchemas(r, c1.getSchemas(), c2.getSchemas())
        );
    }

    private final DiffResult appendSchemas(DiffResult result, List l1, List l2) {
        return append(result, l1, l2, null,
            (r, s) -> r.queries.addAll(Arrays.asList(ctx.ddl(s).queries())),
            (r, s) -> {
                if (s.getTables().isEmpty() && s.getSequences().isEmpty()) {
                    if (!StringUtils.isEmpty(s.getName()))
                        r.queries.add(ctx.dropSchema(s));
                }
                else if (migrateConf.dropSchemaCascade()) {

                    // TODO: Can we reuse the logic from DROP_TABLE?
                    for (Table t1 : s.getTables())
                        for (UniqueKey uk : t1.getKeys())
                            r.droppedFks.addAll(uk.getReferences());

                    if (!StringUtils.isEmpty(s.getName()))
                        r.queries.add(ctx.dropSchema(s).cascade());
                }
                else {
                    for (Table t2 : s.getTables())
                        dropTable().drop(r, t2);

                    for (Sequence seq : s.getSequences())
                        dropSequence().drop(r, seq);

                    if (!StringUtils.isEmpty(s.getName()))
                        r.queries.add(ctx.dropSchema(s));
                }
            },
            (r, s1, s2) -> {
                appendDomains(r, s1.getDomains(), s2.getDomains());
                appendTables(r, s1.getTables(), s2.getTables());
                appendSequences(r, s1.getSequences(), s2.getSequences());
            }
        );
    }

    private final Drop> dropSequence() {
        return (r, s) -> r.queries.add(ctx.dropSequence(s));
    }

    private final DiffResult appendSequences(DiffResult result, List> l1, List> l2) {
        return append(result, l1, l2, null,
            (r, s) -> r.queries.add(ddl.createSequence(s)),
            dropSequence(),
            (r, s1, s2) -> {
                AlterSequenceFlagsStep stmt = null;
                AlterSequenceFlagsStep stmt0 = ctx.alterSequence(s1);

                if (s2.getStartWith() != null && !s2.getStartWith().equals(s1.getStartWith()))
                    stmt = defaultIfNull(stmt, stmt0).startWith(s2.getStartWith());
                else if (s2.getStartWith() == null && s1.getStartWith() != null)
                    stmt = defaultIfNull(stmt, stmt0).startWith(1);

                if (s2.getIncrementBy() != null && !s2.getIncrementBy().equals(s1.getIncrementBy()))
                    stmt = defaultIfNull(stmt, stmt0).incrementBy(s2.getIncrementBy());
                else if (s2.getIncrementBy() == null && s1.getIncrementBy() != null)
                    stmt = defaultIfNull(stmt, stmt0).incrementBy(1);

                if (s2.getMinvalue() != null && !s2.getMinvalue().equals(s1.getMinvalue()))
                    stmt = defaultIfNull(stmt, stmt0).minvalue(s2.getMinvalue());
                else if (s2.getMinvalue() == null && s1.getMinvalue() != null)
                    stmt = defaultIfNull(stmt, stmt0).noMinvalue();

                if (s2.getMaxvalue() != null && !s2.getMaxvalue().equals(s1.getMaxvalue()))
                    stmt = defaultIfNull(stmt, stmt0).maxvalue(s2.getMaxvalue());
                else if (s2.getMaxvalue() == null && s1.getMaxvalue() != null)
                    stmt = defaultIfNull(stmt, stmt0).noMaxvalue();

                if (s2.getCache() != null && !s2.getCache().equals(s1.getCache()))
                    stmt = defaultIfNull(stmt, stmt0).cache(s2.getCache());
                else if (s2.getCache() == null && s1.getCache() != null)
                    stmt = defaultIfNull(stmt, stmt0).noCache();

                if (s2.getCycle() && !s1.getCycle())
                    stmt = defaultIfNull(stmt, stmt0).cycle();
                else if (!s2.getCycle() && s1.getCycle())
                    stmt = defaultIfNull(stmt, stmt0).noCycle();

                if (stmt != null)
                    r.queries.add(stmt);
            }
        );
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    private final DiffResult appendDomains(DiffResult result, List> l1, List> l2) {
        return append(result, l1, l2, null,
            (r, d) -> r.queries.add(ddl.createDomain(d)),
            (r, d) -> r.queries.add(ctx.dropDomain(d)),
            (r, d1, d2) -> {
                if (!d1.getDataType().getSQLDataType().equals(d2.getDataType().getSQLDataType())) {
                    r.queries.addAll(Arrays.asList(ctx.dropDomain(d1), ddl.createDomain(d2)));
                }
                else {
                    if (d1.getDataType().defaulted() && !d2.getDataType().defaulted())
                        r.queries.add(ctx.alterDomain(d1).dropDefault());
                    else if (d2.getDataType().defaulted() && !d2.getDataType().defaultValue().equals(d1.getDataType().defaultValue()))
                        r.queries.add(ctx.alterDomain(d1).setDefault((Field) d2.getDataType().defaultValue()));

                    appendChecks(r, d1, d1.getChecks(), d2.getChecks());
                }
            }
        );
    }

    private final Create> createTable() {
        return (r, t) -> r.queries.addAll(Arrays.asList(ddl.queries(t).queries()));
    }

    private final Drop> dropTable() {
        return (r, t) -> {
            for (UniqueKey uk : t.getKeys())
                for (ForeignKey fk : uk.getReferences())
                    if (r.droppedFks.add(fk) && !migrateConf.dropTableCascade())
                        r.queries.add(ctx.alterTable(fk.getTable()).dropForeignKey(fk.constraint()));

            if (t.getTableType().isView())
                r.queries.add(ctx.dropView(t));
            else if (t.getTableType() == TableType.TEMPORARY)
                r.queries.add(ctx.dropTemporaryTable(t));
            else
                r.queries.add(migrateConf.dropTableCascade()
                    ? ctx.dropTable(t).cascade()
                    : ctx.dropTable(t));
        };
    }

    private final Merge> MERGE_TABLE = new Merge>() {
        @Override
        public void merge(DiffResult r, Table t1, Table t2) {
            boolean v1 = t1.getTableType().isView();
            boolean v2 = t2.getTableType().isView();

            if (v1 && v2) {
                if (!Arrays.equals(t1.fields(), t2.fields())
                      || t2.getOptions().select() != null && !t2.getOptions().select().equals(t1.getOptions().select())
                      || t2.getOptions().source() != null && !t2.getOptions().source().equals(t1.getOptions().source())) {
                    replaceView(r, t1, t2);
                    return;
                }
            }
            else if (v1 != v2) {
                replaceView(r, t1, t2);
                return;
            }
            else {

                // TODO: The order of dropping / adding these objects might be incorrect
                //       as there could be inter-dependencies.
                appendColumns(r, t1, asList(t1.fields()), asList(t2.fields()));
                appendPrimaryKey(r, t1, asList(t1.getPrimaryKey()), asList(t2.getPrimaryKey()));
                appendUniqueKeys(r, t1, removePrimary(t1.getKeys()), removePrimary(t2.getKeys()));
                appendForeignKeys(r, t1, t1.getReferences(), t2.getReferences());
                appendChecks(r, t1, t1.getChecks(), t2.getChecks());
                appendIndexes(r, t1, t1.getIndexes(), t2.getIndexes());
            }

            String c1 = defaultString(t1.getComment());
            String c2 = defaultString(t2.getComment());

            if (!c1.equals(c2))
                if (v2)
                    r.queries.add(ctx.commentOnView(t2).is(c2));
                else
                    r.queries.add(ctx.commentOnTable(t2).is(c2));
        }

        private void replaceView(DiffResult r, Table v1, Table v2) {
            if (!migrateConf.createOrReplaceView())
                dropTable().drop(r, v1);

            createTable().create(r, v2);
        }
    };

    private final DiffResult appendTables(DiffResult result, List> l1, List> l2) {
        return append(result, l1, l2, null, createTable(), dropTable(), MERGE_TABLE);
    }

    private final List> removePrimary(List> list) {
        List> result = new ArrayList<>();

        for (UniqueKey uk : list)
            if (!uk.isPrimary())
                result.add(uk);

        return result;
    }

    private final boolean isSynthetic(Field f) {
        switch (ctx.family()) {









        }

        return false;
    }

    private final boolean isSynthetic(UniqueKey pk) {
        switch (ctx.family()) {







        }

        return false;
    }

    private final DiffResult appendColumns(DiffResult result, Table t1, List> l1, List> l2) {
        final List> add = new ArrayList<>();
        final List> drop = new ArrayList<>();

        result = append(result, l1, l2, null,
            (r, f) -> {

                // Ignore synthetic columns
                if (isSynthetic(f))
                    ;
                else if (migrateConf.alterTableAddMultiple())
                    add.add(f);
                else
                    r.queries.add(ctx.alterTable(t1).add(f));
            },

            (r, f) -> {

                // Ignore synthetic columns
                if (isSynthetic(f))
                    ;
                else if (migrateConf.alterTableDropMultiple())
                    drop.add(f);
                else
                    r.queries.add(ctx.alterTable(t1).drop(f));
            },

            new Merge>() {
                @SuppressWarnings({ "unchecked", "rawtypes" })
                @Override
                public void merge(DiffResult r, Field f1, Field f2) {
                    DataType type1 = f1.getDataType();
                    DataType type2 = f2.getDataType();

                    // TODO: Some dialects support changing nullability and types in one statement
                    //       We should produce a single statement as well, and handle derived things
                    //       like nullability through emulations
                    if (typeNameDifference(type1, type2))
                        r.queries.add(ctx.alterTable(t1).alter(f1).set(type2.nullability(Nullability.DEFAULT)));

                    if (type1.nullable() && !type2.nullable())
                        r.queries.add(ctx.alterTable(t1).alter(f1).setNotNull());
                    else if (!type1.nullable() && type2.nullable())
                        r.queries.add(ctx.alterTable(t1).alter(f1).dropNotNull());

                    Field d1 = type1.defaultValue();
                    Field d2 = type2.defaultValue();

                    if (type1.defaulted() && !type2.defaulted())
                        r.queries.add(ctx.alterTable(t1).alter(f1).dropDefault());
                    else if (type2.defaulted() && (!type1.defaulted() || !d2.equals(d1)))
                        r.queries.add(ctx.alterTable(t1).alter(f1).setDefault((Field) d2));

                    if ((type1.hasLength() && type2.hasLength() && (type1.lengthDefined() != type2.lengthDefined() || type1.length() != type2.length()))
                        || (type1.hasPrecision() && type2.hasPrecision() && precisionDifference(type1, type2))
                        || (type1.hasScale() && type2.hasScale() && (type1.scaleDefined() != type2.scaleDefined() || type1.scale() != type2.scale())))
                        r.queries.add(ctx.alterTable(t1).alter(f1).set(type2));

                    // [#9656] TODO: Change collation
                    // [#9656] TODO: Change character set
                }

                private final boolean typeNameDifference(DataType type1, DataType type2) {
                    if (type1.getTypeName().equals(type2.getTypeName()))
                        return false;

                    // [#10864] In most dialects, DECIMAL and NUMERIC are aliases and don't need to be changed into each other
                    else
                        return type1.getType() != BigDecimal.class || type2.getType() != BigDecimal.class;
                }

                private final boolean precisionDifference(DataType type1, DataType type2) {

                    // [#10807] Only one type has a default precision defined
                    boolean d1 = defaultPrecision(type1);
                    boolean d2 = defaultPrecision(type2);

                    if (d1 || d2)
                        return d1 != d2;
                    else
                        return type1.precision() != type2.precision();
                }

                private final boolean defaultPrecision(DataType type) {
                    if (!type.isDateTime())
                        return false;

                    if (!type.precisionDefined())
                        return true;

                    if (NO_SUPPORT_TIMESTAMP_PRECISION.contains(ctx.dialect()))
                        return true;

                    if (FALSE.equals(ctx.settings().isMigrationIgnoreDefaultTimestampPrecisionDiffs()))
                        return false;

                    switch (ctx.family()) {
                        case MARIADB:
                            return type.precision() == 0;

                        // [#10807] TODO: Alternative defaults will be listed here as they are discovered
                        default:
                            return type.precision() == 6;
                    }
                }
            }
        );

        if (!drop.isEmpty())
            result.queries.add(0, ctx.alterTable(t1).drop(drop));

        if (!add.isEmpty())
            result.queries.add(ctx.alterTable(t1).add(add));

        return result;
    }

    private final DiffResult appendPrimaryKey(DiffResult result, final Table t1, List> pk1, List> pk2) {
        final Create> create = (r, pk) -> {
            if (isSynthetic(pk))
                ;
            else
                r.queries.add(ctx.alterTable(t1).add(pk.constraint()));
        };

        final Drop> drop = (r, pk) -> {
            if (isSynthetic(pk))
                ;
            else if (isEmpty(pk.getName()))
                r.queries.add(ctx.alterTable(t1).dropPrimaryKey());
            else
                r.queries.add(ctx.alterTable(t1).dropPrimaryKey(pk.constraint()));
        };

        return append(result, pk1, pk2, KEY_COMP,
            create,
            drop,
            keyMerge(t1, create, drop, PRIMARY_KEY),
            true
        );
    }

    private final DiffResult appendUniqueKeys(DiffResult result, final Table t1, List> uk1, List> uk2) {
        final Create> create = (r, u) -> r.queries.add(ctx.alterTable(t1).add(u.constraint()));
        final Drop> drop = (r, u) -> r.queries.add(ctx.alterTable(t1).dropUnique(u.constraint()));

        return append(result, uk1, uk2, KEY_COMP,
            create,
            drop,
            keyMerge(t1, create, drop, UNIQUE),
            true
        );
    }

    private final  Merge keyMerge(Table t1, Create create, Drop drop, ConstraintType type) {
        return (r, k1, k2) -> {
            Name n1 = k1.getUnqualifiedName();
            Name n2 = k2.getUnqualifiedName();

            if (n1.empty() ^ n2.empty()) {
                drop.drop(r, k1);
                create.create(r, k2);

                return;
            }

            if (NAMED_COMP.compare(k1, k2) != 0)

                // [#10813] Don't rename constraints in MySQL
                if (type != PRIMARY_KEY || !NO_SUPPORT_PK_NAMES.contains(ctx.dialect()))
                    r.queries.add(ctx.alterTable(t1).renameConstraint(n1).to(n2));











        };
    }

    private final  Merge keyMerge(Domain d1, Create create, Drop drop) {
        return (r, k1, k2) -> {
            Name n1 = k1.getUnqualifiedName();
            Name n2 = k2.getUnqualifiedName();

            if (n1.empty() ^ n2.empty()) {
                drop.drop(r, k1);
                create.create(r, k2);

                return;
            }

            if (NAMED_COMP.compare(k1, k2) != 0)
                r.queries.add(ctx.alterDomain(d1).renameConstraint(n1).to(n2));
        };
    }

    private final DiffResult appendForeignKeys(DiffResult result, final Table t1, List> fk1, List> fk2) {
        final Create> create = (r, fk) -> r.queries.add(ctx.alterTable(t1).add(fk.constraint()));
        final Drop> drop = (r, fk) -> {
            if (r.droppedFks.add(fk))
                r.queries.add(ctx.alterTable(t1).dropForeignKey(fk.constraint()));
        };

        return append(result, fk1, fk2, FOREIGN_KEY_COMP,
            create,
            drop,
            keyMerge(t1, create, drop, FOREIGN_KEY),
            true
        );
    }

    private final DiffResult appendChecks(DiffResult result, Table t1, List> c1, List> c2) {
        final Create> create = (r, c) -> r.queries.add(ctx.alterTable(t1).add(c.constraint()));
        final Drop> drop = (r, c) -> r.queries.add(ctx.alterTable(t1).drop(c.constraint()));

        return append(result, c1, c2, CHECK_COMP,
            create,
            drop,
            keyMerge(t1, create, drop, CHECK),
            true
        );
    }

    private final DiffResult appendChecks(DiffResult result, Domain d1, List> c1, List> c2) {
        final Create> create = (r, c) -> r.queries.add(ctx.alterDomain(d1).add(c.constraint()));
        final Drop> drop = (r, c) -> r.queries.add(ctx.alterDomain(d1).dropConstraint(c.constraint()));

        return append(result, c1, c2, CHECK_COMP,
            create,
            drop,
            keyMerge(d1, create, drop),
            true
        );
    }

    private final DiffResult appendIndexes(DiffResult result, Table t1, List l1, List l2) {
        final Create create = (r, i) -> r.queries.add((i.getUnique() ? ctx.createUniqueIndex(i) : ctx.createIndex(i)).on(t1, i.getFields()));
        final Drop drop = (r, i) -> r.queries.add(ctx.dropIndex(i).on(t1));

        return append(result, l1, l2, INDEX_COMP,
            create,
            drop,
            (r, ix1, ix2) -> {
                if (INDEX_COMP.compare(ix1, ix2) != 0) {
                    drop.drop(r, ix1);
                    create.create(r, ix2);
                }
                else if (NAMED_COMP.compare(ix1, ix2) != 0)
                    r.queries.add(ctx.alterTable(t1).renameIndex(ix1).to(ix2));
            },
            true
        );
    }

    private final  DiffResult append(
        DiffResult result,
        List l1,
        List l2,
        Comparator comp,
        Create create,
        Drop drop,
        Merge merge
    ) {
        return append(result, l1, l2, comp, create, drop, merge, false);
    }

    private final  DiffResult append(
        DiffResult result,
        List l1,
        List l2,
        Comparator comp,
        Create create,
        Drop drop,
        Merge merge,
        boolean dropMergeCreate
    ) {
        if (comp == null)
            comp = NAMED_COMP;

        N s1 = null;
        N s2 = null;

        Iterator i1 = sorted(l1, comp);
        Iterator i2 = sorted(l2, comp);

        DiffResult dropped = dropMergeCreate ? new DiffResult(new ArrayList<>(), result.droppedFks) : result;
        DiffResult merged = dropMergeCreate ? new DiffResult(new ArrayList<>(), result.droppedFks) : result;
        DiffResult created = dropMergeCreate ? new DiffResult(new ArrayList<>(), result.droppedFks) : result;

        for (;;) {
            if (s1 == null && i1.hasNext())
                s1 = i1.next();

            if (s2 == null && i2.hasNext())
                s2 = i2.next();

            if (s1 == null && s2 == null)
                break;

            int c = s1 == null
                  ? 1
                  : s2 == null
                  ? -1
                  : comp.compare(s1, s2);

            if (c < 0) {
                if (drop != null)
                    drop.drop(dropped, s1);

                s1 = null;
            }
            else if (c > 0) {
                if (create != null)
                    create.create(created, s2);

                s2 = null;
            }
            else {
                if (merge != null)
                    merge.merge(merged, s1, s2);

                s1 = s2 = null;
            }
        }

        if (dropMergeCreate) {
            result.addAll(dropped);
            result.addAll(merged);
            result.addAll(created);
        }

        return result;
    }

    private static interface Create {
        void create(DiffResult result, N named);
    }

    private static interface Drop {
        void drop(DiffResult result, N named);
    }

    private static interface Merge {
        void merge(DiffResult result, N named1, N named2);
    }

    private static final  Iterator sorted(List list, Comparator comp) {
        List result = new ArrayList<>(list);
        result.sort(comp);
        return result.iterator();
    }

    private static final record DiffResult(List queries, Set> droppedFks) {
        DiffResult() {
            this(new ArrayList<>(), new HashSet<>());
        }

        void addAll(DiffResult other) {
            queries.addAll(other.queries);
            droppedFks.addAll(other.droppedFks);
        }

        @Override
        public String toString() {
            return queries.toString();
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy