Line 11... |
Line 11... |
11 |
* When distributing the software, include this License Header Notice in each file.
|
11 |
* When distributing the software, include this License Header Notice in each file.
|
12 |
*/
|
12 |
*/
|
13 |
|
13 |
|
14 |
package org.openconcerto.sql.utils;
|
14 |
package org.openconcerto.sql.utils;
|
15 |
|
15 |
|
- |
|
16 |
import org.openconcerto.sql.Log;
|
16 |
import org.openconcerto.sql.model.ConnectionHandlerNoSetup;
|
17 |
import org.openconcerto.sql.model.ConnectionHandlerNoSetup;
|
17 |
import org.openconcerto.sql.model.FieldRef;
|
18 |
import org.openconcerto.sql.model.FieldRef;
|
18 |
import org.openconcerto.sql.model.SQLBase;
|
19 |
import org.openconcerto.sql.model.SQLBase;
|
19 |
import org.openconcerto.sql.model.SQLDataSource;
|
20 |
import org.openconcerto.sql.model.SQLDataSource;
|
20 |
import org.openconcerto.sql.model.SQLField;
|
21 |
import org.openconcerto.sql.model.SQLField;
|
- |
|
22 |
import org.openconcerto.sql.model.SQLRow;
|
21 |
import org.openconcerto.sql.model.SQLSelect;
|
23 |
import org.openconcerto.sql.model.SQLSelect;
|
22 |
import org.openconcerto.sql.model.SQLSyntax;
|
24 |
import org.openconcerto.sql.model.SQLSyntax;
|
23 |
import org.openconcerto.sql.model.SQLSystem;
|
25 |
import org.openconcerto.sql.model.SQLSystem;
|
24 |
import org.openconcerto.sql.model.SQLTable;
|
26 |
import org.openconcerto.sql.model.SQLTable;
|
25 |
import org.openconcerto.sql.model.Where;
|
27 |
import org.openconcerto.sql.model.Where;
|
26 |
import org.openconcerto.sql.request.UpdateBuilder;
|
28 |
import org.openconcerto.sql.request.UpdateBuilder;
|
- |
|
29 |
import org.openconcerto.utils.DecimalUtils;
|
- |
|
30 |
import org.openconcerto.utils.Tuple2;
|
- |
|
31 |
import org.openconcerto.utils.Tuple2.List2;
|
27 |
import org.openconcerto.utils.convertor.NumberConvertor;
|
32 |
import org.openconcerto.utils.convertor.NumberConvertor;
|
28 |
|
33 |
|
29 |
import java.math.BigDecimal;
|
34 |
import java.math.BigDecimal;
|
30 |
import java.sql.Connection;
|
35 |
import java.sql.Connection;
|
31 |
import java.sql.SQLException;
|
36 |
import java.sql.SQLException;
|
32 |
import java.sql.Statement;
|
37 |
import java.sql.Statement;
|
- |
|
38 |
import java.util.ArrayList;
|
33 |
import java.util.Collections;
|
39 |
import java.util.Collections;
|
34 |
import java.util.List;
|
40 |
import java.util.List;
|
- |
|
41 |
import java.util.logging.Level;
|
- |
|
42 |
|
- |
|
43 |
import net.jcip.annotations.GuardedBy;
|
35 |
|
44 |
|
36 |
/**
|
45 |
/**
|
37 |
* Reorder some or all rows of a table.
|
46 |
* Reorder some or all rows of a table.
|
38 |
*
|
47 |
*
|
39 |
* @author Sylvain
|
48 |
* @author Sylvain
|
40 |
*/
|
49 |
*/
|
41 |
public abstract class ReOrder {
|
50 |
public abstract class ReOrder {
|
42 |
|
51 |
|
- |
|
52 |
@GuardedBy("this")
|
- |
|
53 |
private static boolean AUTO_FIX_NULLS = false;
|
- |
|
54 |
|
- |
|
55 |
public static synchronized void setAutoFixNulls(boolean b) {
|
- |
|
56 |
AUTO_FIX_NULLS = b;
|
- |
|
57 |
}
|
- |
|
58 |
|
- |
|
59 |
public static synchronized boolean isAutoFixNulls() {
|
- |
|
60 |
return AUTO_FIX_NULLS;
|
- |
|
61 |
}
|
- |
|
62 |
|
- |
|
63 |
public static BigDecimal makeRoom(final SQLTable t, final BigDecimal roomNeeded, final boolean after, final BigDecimal destOrder) throws SQLException {
|
- |
|
64 |
return makeRoom(t, roomNeeded, after, destOrder, 100);
|
- |
|
65 |
}
|
- |
|
66 |
|
- |
|
67 |
/**
|
- |
|
68 |
* Make sure that there's no rows with order in the passed range. This method accomplishes this
|
- |
|
69 |
* by re-ordering an increasing number of rows. This method only changes orders greater than or
|
- |
|
70 |
* equal to <code>destOrder</code> and the first row re-ordered (<code>destOrder</code> if
|
- |
|
71 |
* <code>!after</code> the next one otherwise) always has <code>destOrder + roomNeeded</code> as
|
- |
|
72 |
* order :
|
- |
|
73 |
*
|
- |
|
74 |
* <pre>
|
- |
|
75 |
* "row foo" 1.0
|
- |
|
76 |
* "row bar" 2.0
|
- |
|
77 |
* "row baz" 3.0
|
- |
|
78 |
* "row qux" 4.0
|
- |
|
79 |
* If <code>roomNeeded</code> is 2 after order 2.0, then the new values will be :
|
- |
|
80 |
* "row foo" 1.0
|
- |
|
81 |
* "row bar" 2.0
|
- |
|
82 |
* "row baz" 4.0
|
- |
|
83 |
* "row qux" 5.0
|
- |
|
84 |
* If on the other hand, one wants the room before 2.0, then :
|
- |
|
85 |
* "row foo" 1.0
|
- |
|
86 |
* "row bar" 4.0
|
- |
|
87 |
* "row baz" 5.0
|
- |
|
88 |
* "row qux" 6.0
|
- |
|
89 |
* </pre>
|
- |
|
90 |
*
|
- |
|
91 |
* @param t the table.
|
- |
|
92 |
* @param roomNeeded the size of the requested free range, e.g 10.
|
- |
|
93 |
* @param after <code>true</code> if the free range should begin after <code>destOrder</code>,
|
- |
|
94 |
* <code>false</code> if it should end before <code>destOrder</code>.
|
- |
|
95 |
* @param destOrder the start or end of the range.
|
- |
|
96 |
* @param initialCount the initial size of the range to re-order if there's no room.
|
- |
|
97 |
* @return the smallest possibly used order <code>>=</code> destOrder.
|
- |
|
98 |
* @throws SQLException if an error occurs.
|
- |
|
99 |
*/
|
- |
|
100 |
public static BigDecimal makeRoom(final SQLTable t, final BigDecimal roomNeeded, final boolean after, final BigDecimal destOrder, final int initialCount) throws SQLException {
|
- |
|
101 |
if (roomNeeded.signum() <= 0)
|
- |
|
102 |
throw new IllegalArgumentException("Negative roomNeeded");
|
- |
|
103 |
if (initialCount < 1)
|
- |
|
104 |
throw new IllegalArgumentException("Initial count too small");
|
- |
|
105 |
final BigDecimal newFirst = destOrder.add(roomNeeded);
|
- |
|
106 |
// reorder to squeeze rows upwards
|
- |
|
107 |
// since we keep increasing count, we will eventually reorder all rows afterwards
|
- |
|
108 |
// NOTE since we only go in one direction (from destOrder and upwards), there shouldn't be
|
- |
|
109 |
// any DB deadlocks
|
- |
|
110 |
int count = Math.max(initialCount, roomNeeded.intValue() + 1);
|
- |
|
111 |
final int tableRowCount = t.getRowCount();
|
- |
|
112 |
boolean reordered = false;
|
- |
|
113 |
while (!reordered) {
|
- |
|
114 |
// only push destRow upwards if we want to add before
|
- |
|
115 |
reordered = ReOrder.create(t, destOrder, !after, count, newFirst).exec();
|
- |
|
116 |
if (!reordered && count > tableRowCount)
|
- |
|
117 |
throw new IllegalStateException("Unable to reorder " + count + " rows in " + t);
|
- |
|
118 |
count *= 10;
|
- |
|
119 |
}
|
- |
|
120 |
return after ? destOrder : newFirst;
|
- |
|
121 |
}
|
- |
|
122 |
|
- |
|
123 |
/**
|
- |
|
124 |
* Get a number of free order values after/before the passed row,
|
- |
|
125 |
* {@link #makeRoom(SQLTable, BigDecimal, boolean, BigDecimal, int) making room} if needed.
|
- |
|
126 |
*
|
- |
|
127 |
* @param rowCount the number of order values needed.
|
- |
|
128 |
* @param after <code>true</code> if the free values should begin after <code>r</code>,
|
- |
|
129 |
* <code>false</code> if they should end before <code>r</code>.
|
- |
|
130 |
* @param r the row that is before or after the returned orders.
|
- |
|
131 |
* @return a list of <code>rowCount</code> free orders and the new order for the passed row
|
- |
|
132 |
* (only changed if there was not enough free values).
|
- |
|
133 |
* @throws SQLException if an error occurs.
|
- |
|
134 |
*/
|
- |
|
135 |
public static Tuple2<List<BigDecimal>, BigDecimal> getFreeOrderValuesFor(final int rowCount, final boolean after, final SQLRow r) throws SQLException {
|
- |
|
136 |
return getFreeOrderValuesFor(rowCount, after, r, isAutoFixNulls());
|
- |
|
137 |
}
|
- |
|
138 |
|
- |
|
139 |
public static Tuple2<List<BigDecimal>, BigDecimal> getFreeOrderValuesFor(final int rowCount, final boolean after, final SQLRow r, final boolean autoFixNulls) throws SQLException {
|
- |
|
140 |
if (rowCount == 0)
|
- |
|
141 |
return Tuple2.<List<BigDecimal>, BigDecimal> create(Collections.<BigDecimal> emptyList(), null);
|
- |
|
142 |
// both rows are locked FOR UPDATE, so there shouldn't be any row that can get between them
|
- |
|
143 |
// in this transaction, as the only way to do that is to call fetchThisAndSequentialRow()
|
- |
|
144 |
List2<SQLRow> seqRows = r.fetchThisAndSequentialRow(after);
|
- |
|
145 |
if (seqRows == null)
|
- |
|
146 |
throw new IllegalStateException("Couldn't find " + r);
|
- |
|
147 |
assert seqRows.get0().equals(r) : "fetchThisAndSequentialRow() failed";
|
- |
|
148 |
if (seqRows.get0().getOrder() == null) {
|
- |
|
149 |
if (autoFixNulls)
|
- |
|
150 |
Log.get().log(Level.WARNING, "Re-order table with null orders : " + r);
|
- |
|
151 |
else
|
- |
|
152 |
throw new IllegalStateException("Row with null order : " + r);
|
- |
|
153 |
if (!ReOrder.create(r.getTable()).exec())
|
- |
|
154 |
throw new IllegalStateException("Couldn't re-order table with null orders : " + r.getTable());
|
- |
|
155 |
seqRows = r.fetchThisAndSequentialRow(after);
|
- |
|
156 |
if (seqRows == null || seqRows.get0().getOrder() == null)
|
- |
|
157 |
throw new IllegalStateException("Re-order table with null orders failed : " + seqRows);
|
- |
|
158 |
}
|
- |
|
159 |
final BigDecimal destOrder = seqRows.get0().getOrder();
|
- |
|
160 |
if (destOrder.compareTo(ReOrder.MIN_ORDER) < 0)
|
- |
|
161 |
throw new IllegalStateException(seqRows.get0() + " has invalid order : " + destOrder);
|
- |
|
162 |
BigDecimal newRowOrder = destOrder;
|
- |
|
163 |
final SQLRow otherRow = seqRows.get1();
|
- |
|
164 |
final BigDecimal inc;
|
- |
|
165 |
BigDecimal newOrder;
|
- |
|
166 |
if (after && otherRow == null) {
|
- |
|
167 |
// dernière ligne de la table
|
- |
|
168 |
inc = ReOrder.DISTANCE;
|
- |
|
169 |
newOrder = destOrder.add(inc);
|
- |
|
170 |
} else {
|
- |
|
171 |
final BigDecimal otherOrder;
|
- |
|
172 |
if (otherRow != null) {
|
- |
|
173 |
otherOrder = otherRow.getOrder();
|
- |
|
174 |
} else {
|
- |
|
175 |
// première ligne
|
- |
|
176 |
otherOrder = ReOrder.MIN_ORDER;
|
- |
|
177 |
}
|
- |
|
178 |
if (otherOrder.compareTo(ReOrder.MIN_ORDER) < 0)
|
- |
|
179 |
throw new IllegalStateException(otherRow + " has invalid order : " + otherOrder);
|
- |
|
180 |
|
- |
|
181 |
// ULP * 10 to give a little breathing room
|
- |
|
182 |
final BigDecimal minDistance = r.getTable().getOrderULP().scaleByPowerOfTen(1);
|
- |
|
183 |
final BigDecimal places = BigDecimal.valueOf(rowCount + 1);
|
- |
|
184 |
// the minimum room to fit rowCount
|
- |
|
185 |
final BigDecimal roomNeeded = minDistance.multiply(places);
|
- |
|
186 |
final BigDecimal roomAvailable = otherOrder.subtract(destOrder).abs();
|
- |
|
187 |
|
- |
|
188 |
if (roomAvailable.compareTo(roomNeeded) < 0) {
|
- |
|
189 |
newRowOrder = makeRoom(r.getTable(), roomNeeded, after, destOrder);
|
- |
|
190 |
inc = minDistance;
|
- |
|
191 |
newOrder = after ? destOrder.add(inc) : destOrder;
|
- |
|
192 |
} else {
|
- |
|
193 |
inc = roomAvailable.divide(places, DecimalUtils.HIGH_PRECISION);
|
- |
|
194 |
newOrder = (after ? destOrder : otherOrder).add(inc);
|
- |
|
195 |
}
|
- |
|
196 |
}
|
- |
|
197 |
assert inc.signum() > 0;
|
- |
|
198 |
final List<BigDecimal> orders = new ArrayList<>(rowCount);
|
- |
|
199 |
for (int i = 0; i < rowCount; i++) {
|
- |
|
200 |
orders.add(DecimalUtils.round(newOrder, r.getTable().getOrderDecimalDigits()));
|
- |
|
201 |
newOrder = newOrder.add(inc);
|
- |
|
202 |
}
|
- |
|
203 |
assert after && newRowOrder.compareTo(orders.get(0)) < 0 || !after && orders.get(rowCount - 1).compareTo(newRowOrder) < 0;
|
- |
|
204 |
return Tuple2.create(orders, newRowOrder);
|
- |
|
205 |
}
|
- |
|
206 |
|
43 |
// must be zero so that we can work on negative numbers without breaking the unique constraint
|
207 |
// must be zero so that we can work on negative numbers without breaking the unique constraint
|
44 |
public static final BigDecimal MIN_ORDER = BigDecimal.ZERO;
|
208 |
public static final BigDecimal MIN_ORDER = BigDecimal.ZERO;
|
45 |
// preferred distance
|
209 |
// preferred distance
|
46 |
public static final BigDecimal DISTANCE = BigDecimal.ONE;
|
210 |
public static final BigDecimal DISTANCE = BigDecimal.ONE;
|
47 |
|
211 |
|
Line 121... |
Line 285... |
121 |
|
285 |
|
122 |
public abstract List<String> getSQL(Connection conn, BigDecimal inc) throws SQLException;
|
286 |
public abstract List<String> getSQL(Connection conn, BigDecimal inc) throws SQLException;
|
123 |
|
287 |
|
124 |
// MAYBE return affected IDs
|
288 |
// MAYBE return affected IDs
|
125 |
public final boolean exec() throws SQLException {
|
289 |
public final boolean exec() throws SQLException {
|
126 |
final UpdateBuilder updateUndef = new UpdateBuilder(this.t).setObject(this.t.getOrderField(), MIN_ORDER);
|
- |
|
127 |
updateUndef.setWhere(new Where(this.t.getKey(), "=", this.t.getUndefinedID()));
|
290 |
final SQLTable t = this.t;
|
128 |
return (Boolean) SQLUtils.executeAtomic(this.t.getBase().getDataSource(), new ConnectionHandlerNoSetup<Object, SQLException>() {
|
291 |
return SQLUtils.executeAtomic(this.t.getBase().getDataSource(), new ConnectionHandlerNoSetup<Boolean, SQLException>() {
|
129 |
@Override
|
292 |
@Override
|
130 |
public Object handle(SQLDataSource ds) throws SQLException, SQLException {
|
293 |
public Boolean handle(SQLDataSource ds) throws SQLException, SQLException {
|
131 |
final Connection conn = ds.getConnection();
|
294 |
final Connection conn = ds.getConnection();
|
132 |
final Statement stmt = conn.createStatement();
|
295 |
final Statement stmt = conn.createStatement();
|
133 |
if (isAll()) {
|
296 |
// reorder all, undef must be at 0
|
134 |
// reorder all, undef must be at 0
|
297 |
if (isAll() && t.getUndefinedIDNumber() != null) {
|
- |
|
298 |
final UpdateBuilder updateUndef = new UpdateBuilder(t).setObject(t.getOrderField(), MIN_ORDER);
|
- |
|
299 |
updateUndef.setWhere(new Where(t.getKey(), "=", t.getUndefinedID()));
|
135 |
stmt.execute(updateUndef.asString());
|
300 |
stmt.execute(updateUndef.asString());
|
136 |
}
|
301 |
}
|
137 |
stmt.execute("SELECT " + ReOrder.this.spec.getInc());
|
302 |
stmt.execute("SELECT " + ReOrder.this.spec.getInc());
|
138 |
final BigDecimal inc = NumberConvertor.toBigDecimal((Number) SQLDataSource.SCALAR_HANDLER.handle(stmt.getResultSet()));
|
303 |
final BigDecimal inc = NumberConvertor.toBigDecimal((Number) SQLDataSource.SCALAR_HANDLER.handle(stmt.getResultSet()));
|
139 |
// needed since the cast in getInc() rounds so if the real increment is 0.006 it
|
304 |
// needed since the cast in getInc() rounds so if the real increment is 0.006 it
|
140 |
// might get rounded to 0.01 and thus the last rows will overlap non moved rows
|
305 |
// might get rounded to 0.01 and thus the last rows will overlap non moved rows
|
141 |
if (inc.compareTo(ReOrder.this.t.getOrderULP().scaleByPowerOfTen(1)) < 0)
|
306 |
if (inc.compareTo(t.getOrderULP().scaleByPowerOfTen(1)) < 0)
|
142 |
return false;
|
307 |
return false;
|
143 |
for (final String s : getSQL(conn, inc)) {
|
308 |
for (final String s : getSQL(conn, inc)) {
|
144 |
stmt.execute(s);
|
309 |
stmt.execute(s);
|
145 |
}
|
310 |
}
|
146 |
// MAYBE fire only changed IDs
|
311 |
// MAYBE fire only changed IDs
|
147 |
ReOrder.this.t.fireTableModified(-1, Collections.singletonList(ReOrder.this.t.getOrderField().getName()));
|
312 |
t.fireTableModified(SQLRow.NONEXISTANT_ID, Collections.singletonList(t.getOrderField().getName()));
|
148 |
return true;
|
313 |
return true;
|
149 |
}
|
314 |
}
|
150 |
});
|
315 |
});
|
151 |
}
|
316 |
}
|
152 |
|
317 |
|
Line 172... |
Line 337... |
172 |
// the row with MIN_ORDER cannot be displayed since no row can be moved before it
|
337 |
// the row with MIN_ORDER cannot be displayed since no row can be moved before it
|
173 |
// so don't change it
|
338 |
// so don't change it
|
174 |
if (first.compareTo(MIN_ORDER) <= 0) {
|
339 |
if (first.compareTo(MIN_ORDER) <= 0) {
|
175 |
this.firstToReorder = MIN_ORDER;
|
340 |
this.firstToReorder = MIN_ORDER;
|
176 |
this.firstToReorderInclusive = false;
|
341 |
this.firstToReorderInclusive = false;
|
177 |
// make some room before the first non MIN_ORDER row so that another on can came
|
342 |
// make some room before the first non MIN_ORDER row so that another one can came
|
178 |
// before it
|
343 |
// before it
|
179 |
this.first = MIN_ORDER.add(DISTANCE).max(newFirst);
|
344 |
this.first = MIN_ORDER.add(DISTANCE).max(newFirst);
|
180 |
// try to keep asked value
|
345 |
// try to keep asked value
|
181 |
this.lastToReorder = originalLastToReorder.compareTo(this.first) > 0 ? originalLastToReorder : this.first.add(BigDecimal.valueOf(count));
|
346 |
this.lastToReorder = originalLastToReorder.compareTo(this.first) > 0 ? originalLastToReorder : this.first.add(BigDecimal.valueOf(count));
|
182 |
} else {
|
347 |
} else {
|