Project

General

Profile

SchemaComparator.java

updated version with DatabaseState and Difference - Boris Schegolev, 08/05/2022 02:33 PM

Download (7.98 KB)

 
1
package com.goldencode.p2j.persist.orm;
2

    
3
import com.goldencode.p2j.persist.Database;
4
import com.goldencode.p2j.persist.PersistenceException;
5

    
6
import java.io.IOException;
7
import java.sql.*;
8
import java.time.Instant;
9
import java.util.ArrayList;
10
import java.util.Collection;
11
import java.util.HashSet;
12
import java.util.Objects;
13

    
14
/**
15
 * JDBC schema comparator. Compares current DB schema state with last known state. Last known state may be provided.
16
 */
17
public class SchemaComparator {
18

    
19
    /**
20
     * Get an instance of the Comparator, load the current database state for use in comparison.
21
     *
22
     * @param database database to connect to
23
     * @return Comparator instance
24
     */
25
    public static SchemaComparator saveInitialState(Database database) {
26
        SchemaComparator schemaComparator = new SchemaComparator(database);
27
        schemaComparator.saveLastKnownState(null);
28
        return schemaComparator;
29
    }
30

    
31
    /**
32
     * Get an instance of the Comparator, assuming the current database state is already loaded.
33
     *
34
     * @param database database to connect to
35
     * @return Comparator instance
36
     */
37
    public static SchemaComparator getInstanceExistingState(Database database) {
38
        return new SchemaComparator(database);
39
    }
40

    
41
    private final Database database;
42

    
43
    private SchemaComparator(Database database) {
44
        this.database = database;
45
    }
46

    
47
    /**
48
     * Compare current state with last known state.
49
     *
50
     * @return ComparisonResult instance
51
     * @throws IOException translated from PersistenceException
52
     */
53
    public ComparisonResult compareWithPrevious() throws IOException {
54
        return compare(loadLastKnownState());
55
    }
56

    
57
    /**
58
     * Compare current state with given state.
59
     *
60
     * @param previousState previous known state for comparison
61
     * @return ComparisonResult instance
62
     * @throws IOException translated from PersistenceException
63
     */
64
    public ComparisonResult compare(DatabaseState previousState) throws IOException {
65
        try (Session session = new Session(database))
66
        {
67
            Connection conn = session.getConnection();
68
            ComparisonResult comparisonResult = new ComparisonResult();
69

    
70
            getAndCompareTables(conn, previousState, comparisonResult);
71
            getAndCompareColumns(conn, previousState, comparisonResult);
72
            getAndCompareIndexes(conn, previousState, comparisonResult);
73

    
74
            if (!comparisonResult.isSame())
75
            {
76
                saveLastKnownState(comparisonResult.getState());
77
            }
78

    
79
            return comparisonResult;
80

    
81
        } catch (PersistenceException | SQLException e) {
82
            throw new IOException("Connection failed during comparison", e);
83
        }
84
    }
85

    
86
    private DatabaseState loadLastKnownState() {
87
        // TODO: load
88
        return new DatabaseState(Instant.now());
89
    }
90

    
91
    private void saveLastKnownState(DatabaseState newState) {
92
        // TODO: persist state info
93
    }
94

    
95
    private void getAndCompareTables(Connection conn, DatabaseState previousState, ComparisonResult comparisonResult) throws SQLException {
96
        // Load tables from DB
97
        ResultSet tables = conn.getMetaData()
98
                .getTables(null, null, "%", new String[]{"TABLE"});
99

    
100
        // Put all data into the new State
101
        DatabaseState currentState = comparisonResult.getState();
102
        while (tables.next()) {
103
            currentState.getTables()
104
                    .add(new Table(tables.getString("TABLE_NAME")));
105
        }
106

    
107
        // Compare if old State is available
108
        if (previousState != null)
109
        {
110
            // Detect added
111
            for (Table table : currentState.getTables()) {
112
                if (!previousState.getTables().contains(table)) {
113
                    comparisonResult.getDifferences()
114
                            .add(new Difference(null, table));
115
                }
116
            }
117
            // Detect removed
118
            for (Table table : previousState.getTables()) {
119
                if (!currentState.getTables().contains(table)) {
120
                    comparisonResult.getDifferences()
121
                            .add(new Difference(table, null));
122
                }
123
            }
124
        }
125
    }
126

    
127
    private void getAndCompareColumns(Connection conn, DatabaseState previousState, ComparisonResult comparisonResult) throws SQLException {
128
        // TODO: load info from conn
129
        if (previousState != null)
130
        {
131
            // TODO: compare with previous state
132
        }
133
    }
134

    
135
    private void getAndCompareIndexes(Connection conn, DatabaseState previousState, ComparisonResult comparisonResult) throws SQLException {
136
        // TODO: load info from conn
137
        if (previousState != null)
138
        {
139
            // TODO: compare with previous state
140
        }
141
    }
142

    
143
    /**
144
     * Result of schema comparison.
145
     *
146
     * TODO: could be immutable, requires builder
147
     */
148
    public static class ComparisonResult {
149

    
150
        private final DatabaseState state = new DatabaseState(Instant.now());
151
        private final Collection<Difference> differences = new ArrayList<>();
152

    
153
        public boolean isSame() {
154
            return differences.isEmpty();
155
        }
156

    
157
        public Collection<Difference> getDifferences() {
158
            return differences;
159
        }
160

    
161
        private DatabaseState getState() {
162
            return state;
163
        }
164
    }
165

    
166
    public static class DatabaseState {
167
        private final Instant creationTime;
168
        private final Collection<Table> tables = new HashSet<>();
169

    
170
        public DatabaseState(Instant creationTime) {
171
            this.creationTime = creationTime;
172
        }
173

    
174
        public Collection<Table> getTables() {
175
            return tables;
176
        }
177

    
178
        public Instant getCreationTime() {
179
            return creationTime;
180
        }
181
    }
182

    
183
    public abstract static class Element {
184
        private final String name;
185

    
186
        protected Element(String name) {
187
            this.name = name;
188
        }
189

    
190
        public String getName() {
191
            return name;
192
        }
193

    
194
        @Override
195
        public boolean equals(Object o) {
196
            if (this == o) return true;
197
            if (o == null || getClass() != o.getClass()) return false;
198
            Element element = (Element) o;
199
            return name.equals(element.name);
200
        }
201

    
202
        @Override
203
        public int hashCode() {
204
            return Objects.hash(name);
205
        }
206
    }
207

    
208
    public static class Table extends Element {
209
        private final Collection<Attribute> attributes = new HashSet<>();
210
        private final Collection<Index> indexes = new HashSet<>();
211

    
212
        public Table(String name) {
213
            super(name);
214
        }
215

    
216
        public Collection<Attribute> getAttributes() {
217
            return attributes;
218
        }
219

    
220
        public Collection<Index> getIndexes() {
221
            return indexes;
222
        }
223
    }
224

    
225
    public static class Attribute extends Element {
226
        public Attribute(String name) {
227
            super(name);
228
        }
229
    }
230

    
231
    public static class Index extends Element {
232
        public Index(String name) {
233
            super(name);
234
        }
235
    }
236

    
237
    public static class Difference {
238
        private final Element oldElement;
239
        private final Element newElement;
240

    
241
        public Difference(Element oldElement, Element newElement) {
242
            this.oldElement = oldElement;
243
            this.newElement = newElement;
244

    
245
            if (oldElement == null && newElement == null) {
246
                throw new IllegalArgumentException("New state and old state cannot be both null");
247
            }
248
        }
249

    
250
        public Element getOldElement() {
251
            return oldElement;
252
        }
253

    
254
        public Element getNewElement() {
255
            return newElement;
256
        }
257

    
258
        @Override
259
        public String toString() {
260
            if (getOldElement() == null) {
261
                return "Added " + getNewElement().getName();
262
            } else if (getNewElement() == null) {
263
                return "Removed " + getOldElement().getName();
264
            }
265
            return getOldElement().getName() + " -> " + getNewElement().getName();
266
        }
267
    }
268
}