OpenConcerto

Dépôt officiel du code source de l'ERP OpenConcerto
sonarqube

svn://code.openconcerto.org/openconcerto

Rev

Rev 142 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
17 ilm 1
/*
2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3
 *
182 ilm 4
 * Copyright 2011-2019 OpenConcerto, by ILM Informatique. All rights reserved.
17 ilm 5
 *
6
 * The contents of this file are subject to the terms of the GNU General Public License Version 3
7
 * only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
8
 * copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
9
 * language governing permissions and limitations under the License.
10
 *
11
 * When distributing the software, include this License Header Notice in each file.
12
 */
13
 
14
 package org.openconcerto.sql.changer.convert;
15
 
16
import static org.openconcerto.utils.CollectionUtils.substract;
182 ilm 17
 
17 ilm 18
import org.openconcerto.sql.changer.Changer;
67 ilm 19
import org.openconcerto.sql.model.AliasedTable;
17 ilm 20
import org.openconcerto.sql.model.ConnectionHandlerNoSetup;
21
import org.openconcerto.sql.model.DBStructureItem;
22
import org.openconcerto.sql.model.DBSystemRoot;
23
import org.openconcerto.sql.model.SQLBase;
24
import org.openconcerto.sql.model.SQLDataSource;
25
import org.openconcerto.sql.model.SQLField;
26
import org.openconcerto.sql.model.SQLName;
27
import org.openconcerto.sql.model.SQLRowValues;
67 ilm 28
import org.openconcerto.sql.model.SQLSchema;
17 ilm 29
import org.openconcerto.sql.model.SQLSelect;
30
import org.openconcerto.sql.model.SQLSyntax;
182 ilm 31
import org.openconcerto.sql.model.SQLSystem;
17 ilm 32
import org.openconcerto.sql.model.SQLTable;
67 ilm 33
import org.openconcerto.sql.model.Where;
34
import org.openconcerto.sql.model.graph.DatabaseGraph;
17 ilm 35
import org.openconcerto.sql.model.graph.Link;
67 ilm 36
import org.openconcerto.sql.model.graph.TablesMap;
37
import org.openconcerto.sql.request.UpdateBuilder;
17 ilm 38
import org.openconcerto.sql.utils.AlterTable;
182 ilm 39
import org.openconcerto.sql.utils.ChangeTable;
67 ilm 40
import org.openconcerto.sql.utils.ChangeTable.FCSpec;
17 ilm 41
import org.openconcerto.sql.utils.SQLCreateTable;
42
import org.openconcerto.sql.utils.SQLUtils;
43
import org.openconcerto.utils.CollectionUtils;
44
import org.openconcerto.utils.cc.ITransformer;
45
 
46
import java.sql.SQLException;
47
import java.util.ArrayList;
67 ilm 48
import java.util.Collections;
65 ilm 49
import java.util.HashSet;
17 ilm 50
import java.util.List;
51
import java.util.Set;
52
 
53
/**
54
 * Merge the passed table into the {@link #setDestTable(SQLTable) destination table}. Ie it copies
55
 * all the data to the destination, then update each referencing link, and finally drop the source
56
 * table.
57
 *
58
 * @author Sylvain
59
 */
60
public class MergeTable extends Changer<SQLTable> {
61
 
62
    public static final String MERGE_DEST_TABLE = "merge.destTable";
63
 
64
    private SQLTable destTable;
67 ilm 65
    private final Set<List<String>> forceFF;
17 ilm 66
 
67 ilm 67
    public MergeTable(final DBSystemRoot b) {
17 ilm 68
        super(b);
69
        this.destTable = null;
67 ilm 70
        this.forceFF = new HashSet<List<String>>();
17 ilm 71
    }
72
 
67 ilm 73
    public final void setDestTable(final SQLTable destTable) {
17 ilm 74
        this.destTable = destTable;
75
    }
76
 
67 ilm 77
    public final void forceFF(String ff) {
78
        this.forceFF(Collections.singletonList(ff));
79
    }
80
 
81
    public final void forceFF(List<String> cols) {
82
        this.forceFF.add(cols);
83
    }
84
 
17 ilm 85
    @Override
61 ilm 86
    protected Class<? extends DBStructureItem<?>> getMaxLevel() {
17 ilm 87
        // avoid deleting all tables
88
        return SQLTable.class;
89
    }
90
 
91
    @Override
92
    public void setUpFromSystemProperties() {
93
        super.setUpFromSystemProperties();
94
        final String prop = System.getProperty(MERGE_DEST_TABLE);
95
        if (prop == null)
96
            throw new IllegalStateException("the system property " + MERGE_DEST_TABLE + " is not defined");
97
        this.setDestTable(getSystemRoot().getDesc(prop, SQLTable.class));
98
    }
99
 
67 ilm 100
    @Override
17 ilm 101
    protected void changeImpl(final SQLTable t) throws SQLException {
102
        // print tables right away, so we don't need to repeat them in error msg
103
        this.getStream().println("merging " + t.getSQLName() + " into " + this.destTable.getSQLName() + "... ");
104
 
105
        if (!this.destTable.getChildrenNames().containsAll(t.getChildrenNames()))
106
            throw new IllegalArgumentException(this.destTable.getSQLName() + " lacks " + substract(t.getChildrenNames(), this.destTable.getChildrenNames()));
107
        // check that t is compatible with destTable
108
        final String noLink = t.equalsChildrenNoLink(this.destTable, null);
109
        if (noLink != null)
110
            throw new IllegalArgumentException(noLink);
111
 
112
        // fields to be copied to the destTable
113
        final List<SQLField> fieldsNoPKNoOrder = new ArrayList<SQLField>(t.getFields());
114
        // the primary key will be automatically generated
115
        fieldsNoPKNoOrder.remove(t.getKey());
116
        // ORDER will be at the end (we will offset it)
117
        final SQLField orderF = t.getOrderField();
118
        fieldsNoPKNoOrder.remove(orderF);
119
        fieldsNoPKNoOrder.add(orderF);
120
        final String fields = "(" + CollectionUtils.join(fieldsNoPKNoOrder, ",", new ITransformer<SQLField, String>() {
121
            @Override
67 ilm 122
            public String transformChecked(final SQLField input) {
17 ilm 123
                return SQLBase.quoteIdentifier(input.getName());
124
            }
125
        }) + ")";
126
        final SQLSelect sel = createSelect(t);
127
        // ORDER has to be computed
128
        fieldsNoPKNoOrder.remove(fieldsNoPKNoOrder.size() - 1);
129
        sel.addAllSelect(fieldsNoPKNoOrder);
130
        // offset by max of the destination table to avoid conflicts
131
        sel.addRawSelect(t.getBase().quote("%n + ( SELECT MAX(%n)+100 FROM %f ) ", orderF, this.destTable.getOrderField(), this.destTable), null);
132
 
133
        final SQLSelect selOldIDs = createSelect(t);
134
        selOldIDs.addSelect(t.getKey());
67 ilm 135
        final List<Number> oldIDs = getDS().executeCol(selOldIDs.asString());
17 ilm 136
 
67 ilm 137
        // if we copy no rows, no need to check constraints
138
        final boolean noRowsToMerge = oldIDs.size() == 0;
139
        final DatabaseGraph graph = t.getDBSystemRoot().getGraph();
182 ilm 140
        // Check that transferred data from t still points to the same rows, i.e. that each foreign
141
        // key of t exists in this.destTable and points to the same table.
67 ilm 142
        final Set<Link> selfRefLinks = new HashSet<Link>();
143
        for (final Link l : graph.getForeignLinks(t)) {
144
            final Link destLink = graph.getForeignLink(this.destTable, l.getCols());
145
            if (destLink == null)
146
                throw new IllegalStateException("No link for " + l.getCols() + " in " + this.destTable.getSQL());
147
            final SQLTable destTableTarget = destLink.getTarget();
148
            if (destTableTarget == destLink.getSource()) {
149
                selfRefLinks.add(destLink);
150
            } else if (destTableTarget != l.getTarget()) {
151
                final String s = "Not pointing to the same table for " + l + " " + destTableTarget.getSQL() + " != " + l.getTarget().getSQL();
152
                final List<String> reasonsToContinue = new ArrayList<String>();
153
                if (noRowsToMerge)
154
                    reasonsToContinue.add("but source table is empty");
155
                if (l.getTarget().getRowCount(false) == 0)
156
                    reasonsToContinue.add("but the link target is empty");
157
                if (this.forceFF.contains(l.getCols()))
158
                    reasonsToContinue.add("but link is forced");
159
 
160
                if (reasonsToContinue.size() == 0)
161
                    throw new IllegalStateException(s);
162
 
163
                getStream().println("WARNING: " + s);
164
                getStream().println(CollectionUtils.join(reasonsToContinue, ";\n"));
165
            }
166
        }
167
 
182 ilm 168
        if (this.isDryRun())
169
            return;
170
 
142 ilm 171
        final SQLSyntax syntax = t.getDBSystemRoot().getSyntax();
182 ilm 172
        final TablesMap tables = new TablesMap();
173
        SQLUtils.executeAtomic(t.getDBSystemRoot().getDataSource(), new ConnectionHandlerNoSetup<Void, SQLException>() {
17 ilm 174
            @Override
182 ilm 175
            public Void handle(final SQLDataSource ds) throws SQLException {
176
                final Set<SQLTable> toRefresh = new HashSet<SQLTable>();
177
 
67 ilm 178
                // drop self reference links before inserting
179
                final AlterTable dropSelfFK = new AlterTable(MergeTable.this.destTable);
180
                for (final Link selfRef : selfRefLinks) {
181
                    dropSelfFK.dropForeignConstraint(selfRef.getName());
182
                }
183
                if (!dropSelfFK.isEmpty())
184
                    ds.execute(dropSelfFK.asString());
185
 
17 ilm 186
                // copy all data of t into destTable
182 ilm 187
                final List<Number> insertedIDs;
188
                // In H2 v1.3 Statement.getGeneratedKeys() only returns the first ID
189
                if (getSyntax().getSystem() == SQLSystem.H2) {
190
                    final SQLField destPK = MergeTable.this.destTable.getKey();
191
                    // null if table is empty
192
                    final Number maxID = (Number) getDS().executeScalar(new SQLSelect().addSelect(destPK, "max").asString());
193
                    final int insertedCount = SQLRowValues.insertCount(destPK.getTable(), fields + " " + sel.asString());
194
 
195
                    final SQLSelect selNewIDs = createSelect(destPK.getTable());
196
                    selNewIDs.addSelect(destPK);
197
                    if (maxID != null)
198
                        selNewIDs.setWhere(new Where(destPK, ">", maxID));
199
                    insertedIDs = getDS().executeCol(selNewIDs.asString());
200
                    if (insertedIDs.size() != insertedCount)
201
                        throw new IllegalStateException("Expected " + insertedCount + " new IDs but got " + insertedIDs.size());
202
                } else {
203
                    insertedIDs = SQLRowValues.insertIDs(MergeTable.this.destTable, fields + " " + sel.asString());
204
                }
17 ilm 205
                // handle undefined
21 ilm 206
                insertedIDs.add(0, MergeTable.this.destTable.getUndefinedIDNumber());
67 ilm 207
                oldIDs.add(0, t.getUndefinedIDNumber());
17 ilm 208
                final int size = insertedIDs.size();
209
                if (size != oldIDs.size())
210
                    throw new IllegalStateException("size mismatch: " + size + " != " + oldIDs.size());
211
 
212
                // load the mapping in the db
67 ilm 213
                final SQLName mapName = new SQLName(t.getDBRoot().getName(), "MAP_" + MergeTable.class.getSimpleName() + System.currentTimeMillis());
214
                final SQLCreateTable createTable = new SQLCreateTable(t.getDBRoot(), mapName.getName());
17 ilm 215
                createTable.setPlain(true);
67 ilm 216
                // cannot use temporary table since we need a SQLTable for UpdateBuilder
17 ilm 217
                createTable.addColumn("OLD_ID", syntax.getIDType());
218
                createTable.addColumn("NEW_ID", syntax.getIDType());
219
                ds.execute(createTable.asString());
67 ilm 220
                final SQLTable mapT = t.getDBRoot().refetchTable(mapName.getName());
221
 
17 ilm 222
                final StringBuilder sb = new StringBuilder();
223
                for (int i = 0; i < size; i++) {
224
                    sb.append("(" + oldIDs.get(i) + ", " + insertedIDs.get(i) + ")");
225
                    if (i < size - 1)
226
                        sb.append(",");
227
                }
228
                ds.execute(t.getBase().quote("INSERT INTO %i(%i, %i) VALUES" + sb, mapName, "OLD_ID", "NEW_ID"));
229
 
230
                // for each link to t, point it to destTable
67 ilm 231
                for (final Link selfRef : selfRefLinks) {
232
                    toRefresh.add(updateLink(selfRef, mapT));
17 ilm 233
                }
67 ilm 234
                for (final Link refLink : graph.getReferentLinks(t)) {
235
                    // self links are already taken care of
236
                    // (we don't want to update t)
237
                    if (refLink.getSource() != t)
238
                        toRefresh.add(updateLink(refLink, mapT));
239
                }
17 ilm 240
 
241
                // all data has been copied, and every link removed
242
                // we can now safely drop t
243
                ds.execute(t.getBase().quote("DROP TABLE %f", t));
67 ilm 244
                ds.execute("DROP TABLE " + mapName.quote());
17 ilm 245
 
182 ilm 246
                SQLTable.unsetUndefIDs(t.getSchema(), CollectionUtils.createSet(t.getName(), mapName.getName()));
247
 
65 ilm 248
                toRefresh.add(t);
67 ilm 249
                toRefresh.add(mapT);
65 ilm 250
 
182 ilm 251
                final Set<SQLSchema> schemas = new HashSet<SQLSchema>();
252
                for (final SQLTable table : toRefresh) {
253
                    tables.add(table.getDBRoot().getName(), table.getName());
254
                    schemas.add(table.getSchema());
255
                }
256
                for (final SQLSchema schema : schemas) {
257
                    schema.updateVersion();
258
                }
259
 
17 ilm 260
                return null;
261
            }
67 ilm 262
 
263
            public SQLTable updateLink(final Link refLink, final SQLTable mapT) {
264
                final SQLField refKey = refLink.getLabel();
265
                final SQLTable refTable = refKey.getTable();
266
                final SQLDataSource ds = refTable.getDBSystemRoot().getDataSource();
267
                final boolean selfLink = refLink.getSource() == refLink.getTarget();
268
                assert refTable != t;
269
 
270
                // drop constraint
271
                // * if selfLink, already dropped
272
                // * if no name, we can assume that there's no actual constraint
273
                // just that the fwk has inferred the link, so we don't need to drop anything
274
                // if we're mistaken "drop table" will fail (as should the UPDATE) and the
275
                // transaction will be rollbacked
276
                if (!selfLink && refLink.getName() != null) {
277
                    final AlterTable dropFK = new AlterTable(refTable);
278
                    dropFK.dropForeignConstraint(refLink.getName());
279
                    ds.execute(dropFK.asString());
280
                }
281
 
282
                // update the field using the map
283
                final UpdateBuilder update = new UpdateBuilder(refTable);
284
                final AliasedTable alias1 = new AliasedTable(mapT, "m");
182 ilm 285
                // Where.NULL_IS_DATA_EQ since we want to be able to map a null undefined
286
                // ID, i.e. rows pointing to null into 1.
287
                update.addVirtualJoin(alias1, "OLD_ID", Where.NULL_IS_DATA_EQ, refKey.getName());
288
                update.setFromVirtualJoinField(refKey.getName(), alias1.getAlias(), "NEW_ID");
67 ilm 289
                if (selfLink) {
290
                    // only update new rows (old rows can have the same IDs but they point to old
291
                    // foreign rows, they must not be updated)
292
                    final AliasedTable onlyNew = new AliasedTable(mapT, "onlyNew");
182 ilm 293
                    final Where w = new Where(refTable.getKey(), true, new SQLSelect().addSelect(onlyNew.getField("NEW_ID")))
294
                            // we added the undefined to NEW_ID, but it wasn't copied from t so
295
                            // don't update
296
                            .and(new Where(refTable.getKey(), Where.NULL_IS_DATA_NEQ, refTable.getUndefinedIDNumber()));
297
                    update.setWhere(w.and(update.getWhere()));
67 ilm 298
                }
299
                ds.execute(update.asString());
300
 
301
                // re-add constraint
302
                final AlterTable addFK = new AlterTable(refTable);
303
                // don't create an index : if there was one it's still there, if there wasn't
304
                // don't alter the table silently (use AddFK if you want that)
305
                addFK.addForeignConstraint(FCSpec.createFromLink(refLink, MergeTable.this.destTable), false);
182 ilm 306
                // e.g. change from 'integer DEFAULT 1' to 'integer'
307
                addFK.alterColumnDefault(refLink.getSingleField().getName(), ChangeTable.getForeignColumDefaultValue(MergeTable.this.destTable));
67 ilm 308
                ds.execute(addFK.asString());
309
                return refTable;
310
            }
17 ilm 311
        });
67 ilm 312
        t.getDBSystemRoot().refresh(tables, false);
17 ilm 313
    }
314
 
315
    private final SQLSelect createSelect(final SQLTable t) {
67 ilm 316
        final SQLSelect sel = new SQLSelect(true);
17 ilm 317
        // undefined is not copied
318
        sel.setExcludeUndefined(true);
319
        // necessary so that ids are returned in the same order every time
320
        sel.addFieldOrder(t.getOrderField());
321
        return sel;
322
    }
323
 
324
}