1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
|
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.db;
import com.google.common.base.Function;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Ordering;
import com.google.common.collect.Sets;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
import static com.google.common.collect.Lists.newArrayList;
public class DatabaseUtils {
public static final int PARTITION_SIZE_FOR_ORACLE = 1000;
public static void closeQuietly(@Nullable Connection connection) {
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
Loggers.get(DatabaseUtils.class).warn("Fail to close connection", e);
// ignore
}
}
}
public static void closeQuietly(@Nullable Statement stmt) {
if (stmt != null) {
try {
stmt.close();
} catch (SQLException e) {
Loggers.get(DatabaseUtils.class).warn("Fail to close statement", e);
// ignore
}
}
}
public static void closeQuietly(@Nullable ResultSet rs) {
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
Loggers.get(DatabaseUtils.class).warn("Fail to close result set", e);
// ignore
}
}
}
/**
* Returns an escaped value in parameter, with the desired wildcards. Suitable to be used in a like sql query<br />
* Escapes the "/", "%" and "_" characters.<br/>
*
* You <strong>must</strong> add "ESCAPE '/'" after your like query. It defines '/' as the escape character.
*/
public static String buildLikeValue(String value, WildcardPosition wildcardPosition) {
String escapedValue = escapePercentAndUnderscore(value);
String wildcard = "%";
switch (wildcardPosition) {
case BEFORE:
escapedValue = wildcard + escapedValue;
break;
case AFTER:
escapedValue += wildcard;
break;
case BEFORE_AND_AFTER:
escapedValue = wildcard + escapedValue + wildcard;
break;
default:
throw new UnsupportedOperationException("Unhandled WildcardPosition: " + wildcardPosition);
}
return escapedValue;
}
/**
* Replace escape percent and underscore by adding a slash just before
*/
private static String escapePercentAndUnderscore(String value) {
return value
.replaceAll("/", "//")
.replaceAll("%", "/%")
.replaceAll("_", "/_");
}
/**
* Partition by 1000 elements a list of input and execute a function on each part.
*
* The goal is to prevent issue with ORACLE when there's more than 1000 elements in a 'in ('X', 'Y', ...)'
* and with MsSQL when there's more than 2000 parameters in a query
*/
public static <OUTPUT, INPUT extends Comparable<INPUT>> List<OUTPUT> executeLargeInputs(Collection<INPUT> input, Function<List<INPUT>, List<OUTPUT>> function) {
return executeLargeInputs(input, function, size -> size == 0 ? Collections.emptyList() : new ArrayList<>(size));
}
public static <OUTPUT, INPUT extends Comparable<INPUT>> Set<OUTPUT> executeLargeInputsIntoSet(Collection<INPUT> input, Function<List<INPUT>, Set<OUTPUT>> function) {
return executeLargeInputs(input, function, size -> size == 0 ? Collections.emptySet() : new HashSet<>(size));
}
private static <OUTPUT, INPUT extends Comparable<INPUT>, RESULT extends Collection<OUTPUT>> RESULT executeLargeInputs(Collection<INPUT> input,
Function<List<INPUT>, RESULT> function, java.util.function.Function<Integer, RESULT> outputInitializer) {
if (input.isEmpty()) {
return outputInitializer.apply(0);
}
RESULT results = outputInitializer.apply(input.size());
for (List<INPUT> partition : toUniqueAndSortedPartitions(input)) {
RESULT subResults = function.apply(partition);
if (subResults != null) {
results.addAll(subResults);
}
}
return results;
}
/**
* Partition by 1000 elements a list of input and execute a consumer on each part.
*
* The goal is to prevent issue with ORACLE when there's more than 1000 elements in a 'in ('X', 'Y', ...)'
* and with MsSQL when there's more than 2000 parameters in a query
*/
public static <INPUT extends Comparable<INPUT>> void executeLargeUpdates(Collection<INPUT> inputs, Consumer<List<INPUT>> consumer) {
Iterable<List<INPUT>> partitions = toUniqueAndSortedPartitions(inputs);
for (List<INPUT> partition : partitions) {
consumer.accept(partition);
}
}
/**
* Partition by 1000 elements a list of input and execute a consumer on each part.
*
* The goal is to prevent issue with ORACLE when there's more than 1000 elements in a 'in ('X', 'Y', ...)'
* and with MsSQL when there's more than 2000 parameters in a query
*
* @param sqlCaller a {@link Function} which calls the SQL update/delete and returns the number of updated/deleted rows.
*
* @return the total number of updated/deleted rows (computed as the sum of the values returned by {@code sqlCaller}).
*/
public static <INPUT extends Comparable<INPUT>> int executeLargeUpdates(Collection<INPUT> inputs, Function<List<INPUT>, Integer> sqlCaller) {
Iterable<List<INPUT>> partitions = toUniqueAndSortedPartitions(inputs);
Integer res = 0;
for (List<INPUT> partition : partitions) {
res += sqlCaller.apply(partition);
}
return res;
}
/**
* Ensure values {@code inputs} are unique (which avoids useless arguments) and sorted before creating the partition.
*/
private static <INPUT extends Comparable<INPUT>> Iterable<List<INPUT>> toUniqueAndSortedPartitions(Collection<INPUT> inputs) {
return Iterables.partition(toUniqueAndSortedList(inputs), PARTITION_SIZE_FOR_ORACLE);
}
/**
* Ensure values {@code inputs} are unique (which avoids useless arguments) and sorted so that there is little
* variations of SQL requests over time as possible with a IN clause and/or a group of OR clauses. Such requests can
* then be more easily optimized by the SGDB engine.
*/
public static <INPUT extends Comparable<INPUT>> List<INPUT> toUniqueAndSortedList(Iterable<INPUT> inputs) {
if (inputs instanceof Set) {
// inputs are unique but order is not enforced
return Ordering.natural().immutableSortedCopy(inputs);
}
// inputs are not unique and order is not guaranteed
return Ordering.natural().immutableSortedCopy(Sets.newHashSet(inputs));
}
/**
* Partition by 1000 elements a list of input and execute a function on each part.
* The function has not output (ex: delete operation)
*
* The goal is to prevent issue with ORACLE when there's more than 1000 elements in a 'in ('X', 'Y', ...)'
* and with MsSQL when there's more than 2000 parameters in a query
*/
public static <INPUT> void executeLargeInputsWithoutOutput(Collection<INPUT> input, Function<List<INPUT>, Void> function) {
if (input.isEmpty()) {
return;
}
List<List<INPUT>> partitions = Lists.partition(newArrayList(input), PARTITION_SIZE_FOR_ORACLE);
for (List<INPUT> partition : partitions) {
function.apply(partition);
}
}
public static String repeatCondition(String sql, int count, String separator) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < count; i++) {
sb.append(sql);
if (i < count - 1) {
sb.append(" ").append(separator).append(" ");
}
}
return sb.toString();
}
/**
* Logback does not log exceptions associated to {@link java.sql.SQLException#getNextException()}.
* See http://jira.qos.ch/browse/LOGBACK-775
*/
public static void log(Logger logger, SQLException e) {
SQLException next = e.getNextException();
while (next != null) {
logger.error("SQL error: {}. Message: {}", next.getSQLState(), next.getMessage());
next = next.getNextException();
}
}
@CheckForNull
public static Long getLong(ResultSet rs, String columnName) throws SQLException {
long l = rs.getLong(columnName);
return rs.wasNull() ? null : l;
}
@CheckForNull
public static Double getDouble(ResultSet rs, String columnName) throws SQLException {
double d = rs.getDouble(columnName);
return rs.wasNull() ? null : d;
}
@CheckForNull
public static Integer getInt(ResultSet rs, String columnName) throws SQLException {
int i = rs.getInt(columnName);
return rs.wasNull() ? null : i;
}
@CheckForNull
public static String getString(ResultSet rs, String columnName) throws SQLException {
String s = rs.getString(columnName);
return rs.wasNull() ? null : s;
}
@CheckForNull
public static Long getLong(ResultSet rs, int columnIndex) throws SQLException {
long l = rs.getLong(columnIndex);
return rs.wasNull() ? null : l;
}
@CheckForNull
public static Double getDouble(ResultSet rs, int columnIndex) throws SQLException {
double d = rs.getDouble(columnIndex);
return rs.wasNull() ? null : d;
}
@CheckForNull
public static Integer getInt(ResultSet rs, int columnIndex) throws SQLException {
int i = rs.getInt(columnIndex);
return rs.wasNull() ? null : i;
}
@CheckForNull
public static String getString(ResultSet rs, int columnIndex) throws SQLException {
String s = rs.getString(columnIndex);
return rs.wasNull() ? null : s;
}
@CheckForNull
public static Date getDate(ResultSet rs, int columnIndex) throws SQLException {
Timestamp t = rs.getTimestamp(columnIndex);
return rs.wasNull() ? null : new Date(t.getTime());
}
}
|