Dépôt officiel du code source de l'ERP OpenConcerto
Rev 142 | Blame | Compare with Previous | Last modification | View Log | RSS feed
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 2011-2019 OpenConcerto, by ILM Informatique. All rights reserved.
*
* The contents of this file are subject to the terms of the GNU General Public License Version 3
* only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
* copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each file.
*/
package org.openconcerto.sql.changer.convert;
import static org.openconcerto.utils.CollectionUtils.substract;
import org.openconcerto.sql.changer.Changer;
import org.openconcerto.sql.model.AliasedTable;
import org.openconcerto.sql.model.ConnectionHandlerNoSetup;
import org.openconcerto.sql.model.DBStructureItem;
import org.openconcerto.sql.model.DBSystemRoot;
import org.openconcerto.sql.model.SQLBase;
import org.openconcerto.sql.model.SQLDataSource;
import org.openconcerto.sql.model.SQLField;
import org.openconcerto.sql.model.SQLName;
import org.openconcerto.sql.model.SQLRowValues;
import org.openconcerto.sql.model.SQLSchema;
import org.openconcerto.sql.model.SQLSelect;
import org.openconcerto.sql.model.SQLSyntax;
import org.openconcerto.sql.model.SQLSystem;
import org.openconcerto.sql.model.SQLTable;
import org.openconcerto.sql.model.Where;
import org.openconcerto.sql.model.graph.DatabaseGraph;
import org.openconcerto.sql.model.graph.Link;
import org.openconcerto.sql.model.graph.TablesMap;
import org.openconcerto.sql.request.UpdateBuilder;
import org.openconcerto.sql.utils.AlterTable;
import org.openconcerto.sql.utils.ChangeTable;
import org.openconcerto.sql.utils.ChangeTable.FCSpec;
import org.openconcerto.sql.utils.SQLCreateTable;
import org.openconcerto.sql.utils.SQLUtils;
import org.openconcerto.utils.CollectionUtils;
import org.openconcerto.utils.cc.ITransformer;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Merge the passed table into the {@link #setDestTable(SQLTable) destination table}. Ie it copies
* all the data to the destination, then update each referencing link, and finally drop the source
* table.
*
* @author Sylvain
*/
public class MergeTable extends Changer<SQLTable> {
public static final String MERGE_DEST_TABLE = "merge.destTable";
private SQLTable destTable;
private final Set<List<String>> forceFF;
public MergeTable(final DBSystemRoot b) {
super(b);
this.destTable = null;
this.forceFF = new HashSet<List<String>>();
}
public final void setDestTable(final SQLTable destTable) {
this.destTable = destTable;
}
public final void forceFF(String ff) {
this.forceFF(Collections.singletonList(ff));
}
public final void forceFF(List<String> cols) {
this.forceFF.add(cols);
}
@Override
protected Class<? extends DBStructureItem<?>> getMaxLevel() {
// avoid deleting all tables
return SQLTable.class;
}
@Override
public void setUpFromSystemProperties() {
super.setUpFromSystemProperties();
final String prop = System.getProperty(MERGE_DEST_TABLE);
if (prop == null)
throw new IllegalStateException("the system property " + MERGE_DEST_TABLE + " is not defined");
this.setDestTable(getSystemRoot().getDesc(prop, SQLTable.class));
}
@Override
protected void changeImpl(final SQLTable t) throws SQLException {
// print tables right away, so we don't need to repeat them in error msg
this.getStream().println("merging " + t.getSQLName() + " into " + this.destTable.getSQLName() + "... ");
if (!this.destTable.getChildrenNames().containsAll(t.getChildrenNames()))
throw new IllegalArgumentException(this.destTable.getSQLName() + " lacks " + substract(t.getChildrenNames(), this.destTable.getChildrenNames()));
// check that t is compatible with destTable
final String noLink = t.equalsChildrenNoLink(this.destTable, null);
if (noLink != null)
throw new IllegalArgumentException(noLink);
// fields to be copied to the destTable
final List<SQLField> fieldsNoPKNoOrder = new ArrayList<SQLField>(t.getFields());
// the primary key will be automatically generated
fieldsNoPKNoOrder.remove(t.getKey());
// ORDER will be at the end (we will offset it)
final SQLField orderF = t.getOrderField();
fieldsNoPKNoOrder.remove(orderF);
fieldsNoPKNoOrder.add(orderF);
final String fields = "(" + CollectionUtils.join(fieldsNoPKNoOrder, ",", new ITransformer<SQLField, String>() {
@Override
public String transformChecked(final SQLField input) {
return SQLBase.quoteIdentifier(input.getName());
}
}) + ")";
final SQLSelect sel = createSelect(t);
// ORDER has to be computed
fieldsNoPKNoOrder.remove(fieldsNoPKNoOrder.size() - 1);
sel.addAllSelect(fieldsNoPKNoOrder);
// offset by max of the destination table to avoid conflicts
sel.addRawSelect(t.getBase().quote("%n + ( SELECT MAX(%n)+100 FROM %f ) ", orderF, this.destTable.getOrderField(), this.destTable), null);
final SQLSelect selOldIDs = createSelect(t);
selOldIDs.addSelect(t.getKey());
final List<Number> oldIDs = getDS().executeCol(selOldIDs.asString());
// if we copy no rows, no need to check constraints
final boolean noRowsToMerge = oldIDs.size() == 0;
final DatabaseGraph graph = t.getDBSystemRoot().getGraph();
// Check that transferred data from t still points to the same rows, i.e. that each foreign
// key of t exists in this.destTable and points to the same table.
final Set<Link> selfRefLinks = new HashSet<Link>();
for (final Link l : graph.getForeignLinks(t)) {
final Link destLink = graph.getForeignLink(this.destTable, l.getCols());
if (destLink == null)
throw new IllegalStateException("No link for " + l.getCols() + " in " + this.destTable.getSQL());
final SQLTable destTableTarget = destLink.getTarget();
if (destTableTarget == destLink.getSource()) {
selfRefLinks.add(destLink);
} else if (destTableTarget != l.getTarget()) {
final String s = "Not pointing to the same table for " + l + " " + destTableTarget.getSQL() + " != " + l.getTarget().getSQL();
final List<String> reasonsToContinue = new ArrayList<String>();
if (noRowsToMerge)
reasonsToContinue.add("but source table is empty");
if (l.getTarget().getRowCount(false) == 0)
reasonsToContinue.add("but the link target is empty");
if (this.forceFF.contains(l.getCols()))
reasonsToContinue.add("but link is forced");
if (reasonsToContinue.size() == 0)
throw new IllegalStateException(s);
getStream().println("WARNING: " + s);
getStream().println(CollectionUtils.join(reasonsToContinue, ";\n"));
}
}
if (this.isDryRun())
return;
final SQLSyntax syntax = t.getDBSystemRoot().getSyntax();
final TablesMap tables = new TablesMap();
SQLUtils.executeAtomic(t.getDBSystemRoot().getDataSource(), new ConnectionHandlerNoSetup<Void, SQLException>() {
@Override
public Void handle(final SQLDataSource ds) throws SQLException {
final Set<SQLTable> toRefresh = new HashSet<SQLTable>();
// drop self reference links before inserting
final AlterTable dropSelfFK = new AlterTable(MergeTable.this.destTable);
for (final Link selfRef : selfRefLinks) {
dropSelfFK.dropForeignConstraint(selfRef.getName());
}
if (!dropSelfFK.isEmpty())
ds.execute(dropSelfFK.asString());
// copy all data of t into destTable
final List<Number> insertedIDs;
// In H2 v1.3 Statement.getGeneratedKeys() only returns the first ID
if (getSyntax().getSystem() == SQLSystem.H2) {
final SQLField destPK = MergeTable.this.destTable.getKey();
// null if table is empty
final Number maxID = (Number) getDS().executeScalar(new SQLSelect().addSelect(destPK, "max").asString());
final int insertedCount = SQLRowValues.insertCount(destPK.getTable(), fields + " " + sel.asString());
final SQLSelect selNewIDs = createSelect(destPK.getTable());
selNewIDs.addSelect(destPK);
if (maxID != null)
selNewIDs.setWhere(new Where(destPK, ">", maxID));
insertedIDs = getDS().executeCol(selNewIDs.asString());
if (insertedIDs.size() != insertedCount)
throw new IllegalStateException("Expected " + insertedCount + " new IDs but got " + insertedIDs.size());
} else {
insertedIDs = SQLRowValues.insertIDs(MergeTable.this.destTable, fields + " " + sel.asString());
}
// handle undefined
insertedIDs.add(0, MergeTable.this.destTable.getUndefinedIDNumber());
oldIDs.add(0, t.getUndefinedIDNumber());
final int size = insertedIDs.size();
if (size != oldIDs.size())
throw new IllegalStateException("size mismatch: " + size + " != " + oldIDs.size());
// load the mapping in the db
final SQLName mapName = new SQLName(t.getDBRoot().getName(), "MAP_" + MergeTable.class.getSimpleName() + System.currentTimeMillis());
final SQLCreateTable createTable = new SQLCreateTable(t.getDBRoot(), mapName.getName());
createTable.setPlain(true);
// cannot use temporary table since we need a SQLTable for UpdateBuilder
createTable.addColumn("OLD_ID", syntax.getIDType());
createTable.addColumn("NEW_ID", syntax.getIDType());
ds.execute(createTable.asString());
final SQLTable mapT = t.getDBRoot().refetchTable(mapName.getName());
final StringBuilder sb = new StringBuilder();
for (int i = 0; i < size; i++) {
sb.append("(" + oldIDs.get(i) + ", " + insertedIDs.get(i) + ")");
if (i < size - 1)
sb.append(",");
}
ds.execute(t.getBase().quote("INSERT INTO %i(%i, %i) VALUES" + sb, mapName, "OLD_ID", "NEW_ID"));
// for each link to t, point it to destTable
for (final Link selfRef : selfRefLinks) {
toRefresh.add(updateLink(selfRef, mapT));
}
for (final Link refLink : graph.getReferentLinks(t)) {
// self links are already taken care of
// (we don't want to update t)
if (refLink.getSource() != t)
toRefresh.add(updateLink(refLink, mapT));
}
// all data has been copied, and every link removed
// we can now safely drop t
ds.execute(t.getBase().quote("DROP TABLE %f", t));
ds.execute("DROP TABLE " + mapName.quote());
SQLTable.unsetUndefIDs(t.getSchema(), CollectionUtils.createSet(t.getName(), mapName.getName()));
toRefresh.add(t);
toRefresh.add(mapT);
final Set<SQLSchema> schemas = new HashSet<SQLSchema>();
for (final SQLTable table : toRefresh) {
tables.add(table.getDBRoot().getName(), table.getName());
schemas.add(table.getSchema());
}
for (final SQLSchema schema : schemas) {
schema.updateVersion();
}
return null;
}
public SQLTable updateLink(final Link refLink, final SQLTable mapT) {
final SQLField refKey = refLink.getLabel();
final SQLTable refTable = refKey.getTable();
final SQLDataSource ds = refTable.getDBSystemRoot().getDataSource();
final boolean selfLink = refLink.getSource() == refLink.getTarget();
assert refTable != t;
// drop constraint
// * if selfLink, already dropped
// * if no name, we can assume that there's no actual constraint
// just that the fwk has inferred the link, so we don't need to drop anything
// if we're mistaken "drop table" will fail (as should the UPDATE) and the
// transaction will be rollbacked
if (!selfLink && refLink.getName() != null) {
final AlterTable dropFK = new AlterTable(refTable);
dropFK.dropForeignConstraint(refLink.getName());
ds.execute(dropFK.asString());
}
// update the field using the map
final UpdateBuilder update = new UpdateBuilder(refTable);
final AliasedTable alias1 = new AliasedTable(mapT, "m");
// Where.NULL_IS_DATA_EQ since we want to be able to map a null undefined
// ID, i.e. rows pointing to null into 1.
update.addVirtualJoin(alias1, "OLD_ID", Where.NULL_IS_DATA_EQ, refKey.getName());
update.setFromVirtualJoinField(refKey.getName(), alias1.getAlias(), "NEW_ID");
if (selfLink) {
// only update new rows (old rows can have the same IDs but they point to old
// foreign rows, they must not be updated)
final AliasedTable onlyNew = new AliasedTable(mapT, "onlyNew");
final Where w = new Where(refTable.getKey(), true, new SQLSelect().addSelect(onlyNew.getField("NEW_ID")))
// we added the undefined to NEW_ID, but it wasn't copied from t so
// don't update
.and(new Where(refTable.getKey(), Where.NULL_IS_DATA_NEQ, refTable.getUndefinedIDNumber()));
update.setWhere(w.and(update.getWhere()));
}
ds.execute(update.asString());
// re-add constraint
final AlterTable addFK = new AlterTable(refTable);
// don't create an index : if there was one it's still there, if there wasn't
// don't alter the table silently (use AddFK if you want that)
addFK.addForeignConstraint(FCSpec.createFromLink(refLink, MergeTable.this.destTable), false);
// e.g. change from 'integer DEFAULT 1' to 'integer'
addFK.alterColumnDefault(refLink.getSingleField().getName(), ChangeTable.getForeignColumDefaultValue(MergeTable.this.destTable));
ds.execute(addFK.asString());
return refTable;
}
});
t.getDBSystemRoot().refresh(tables, false);
}
private final SQLSelect createSelect(final SQLTable t) {
final SQLSelect sel = new SQLSelect(true);
// undefined is not copied
sel.setExcludeUndefined(true);
// necessary so that ids are returned in the same order every time
sel.addFieldOrder(t.getOrderField());
return sel;
}
}