3 * Copyright (C) 2009-2024 SonarSource SA
4 * mailto:info AT sonarsource DOT com
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 3 of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this program; if not, write to the Free Software Foundation,
18 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 package org.sonar.server.platform.db.migration.version.v108;
22 import com.google.gson.Gson;
23 import java.sql.Connection;
24 import java.sql.SQLException;
25 import java.util.HashMap;
26 import java.util.List;
29 import java.util.concurrent.atomic.AtomicReference;
30 import javax.annotation.Nullable;
31 import org.apache.commons.codec.digest.MurmurHash3;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
34 import org.sonar.api.utils.System2;
35 import org.sonar.db.Database;
36 import org.sonar.db.DatabaseUtils;
37 import org.sonar.server.platform.db.migration.step.DataChange;
38 import org.sonar.server.platform.db.migration.step.MassUpdate;
39 import org.sonar.server.platform.db.migration.step.Select;
40 import org.sonar.server.platform.db.migration.step.Upsert;
42 import static java.lang.String.format;
43 import static java.nio.charset.StandardCharsets.UTF_8;
45 public abstract class AbstractMigrateLiveMeasuresToMeasures extends DataChange {
46 private static final Logger LOGGER = LoggerFactory.getLogger(AbstractMigrateLiveMeasuresToMeasures.class);
48 private static final Set<String> TEXT_VALUE_TYPES = Set.of("STRING", "LEVEL", "DATA", "DISTRIB");
49 private static final Gson GSON = new Gson();
51 private static final String SELECT_QUERY = """
52 SELECT lm.component_uuid,
59 INNER JOIN metrics m ON m.uuid = lm.metric_uuid
60 WHERE lm.project_uuid = ?
61 ORDER BY lm.component_uuid
64 private static final String INSERT_QUERY = """
65 insert into measures (component_uuid, branch_uuid, json_value, json_value_hash, created_at, updated_at)
66 values ( ?, ?, ?, ?, ?, ?)
69 private final String tableName;
70 private final String item;
71 private final System2 system2;
73 protected AbstractMigrateLiveMeasuresToMeasures(Database db, System2 system2, String tableName, String item) {
75 this.system2 = system2;
76 this.tableName = tableName;
80 private String getSelectUuidQuery() {
84 WHERE measures_migrated = ?
88 private String getCountQuery() {
95 private String getUpdateFlagQuery() {
98 SET measures_migrated = ?
104 protected void execute(Context context) throws SQLException {
105 try (Connection c = getDatabase().getDataSource().getConnection()) {
106 // the table is later deleted, this check ensures the migration re-entrance
107 if (!DatabaseUtils.tableExists("live_measures", c)) {
112 List<String> uuids = context.prepareSelect(getSelectUuidQuery())
113 .setBoolean(1, false)
114 .list(row -> row.getString(1));
116 Long total = context.prepareSelect(getCountQuery())
117 .get(row -> row.getLong(1));
119 LOGGER.info("Starting the migration of {} {}s (total number of {}s: {})", uuids.size(), item, item, total);
122 for (String uuid : uuids) {
124 migrateItem(uuid, context);
125 } catch (Exception e) {
126 LOGGER.error(format("Migration of %s %s failed", item, uuid));
131 if (migrated % 100 == 0) {
132 LOGGER.info("{} {}s migrated", migrated, item);
137 private void migrateItem(String uuid, Context context) throws SQLException {
138 LOGGER.debug("Migrating {} {}...", item, uuid);
140 Map<String, Object> measureValues = new HashMap<>();
141 AtomicReference<String> componentUuid = new AtomicReference<>(null);
143 MassUpdate massUpdate = context.prepareMassUpdate();
144 massUpdate.select(SELECT_QUERY).setString(1, uuid);
145 massUpdate.update(INSERT_QUERY);
146 massUpdate.execute((row, update) -> {
147 boolean shouldUpdate = false;
148 String rowComponentUuid = row.getString(1);
149 if (componentUuid.get() == null || !rowComponentUuid.equals(componentUuid.get())) {
150 if (!measureValues.isEmpty()) {
151 preparePersistMeasure(uuid, update, componentUuid, measureValues);
155 LOGGER.debug("Starting processing of component {}...", rowComponentUuid);
156 componentUuid.set(rowComponentUuid);
157 measureValues.clear();
158 readMeasureValue(row, measureValues);
160 readMeasureValue(row, measureValues);
164 // insert the last component
165 if (!measureValues.isEmpty()) {
166 try (Upsert measureInsert = context.prepareUpsert(INSERT_QUERY)) {
167 preparePersistMeasure(uuid, measureInsert, componentUuid, measureValues);
174 LOGGER.debug("Flagging migration done for {} {}...", item, uuid);
176 try (Upsert flagUpdate = context.prepareUpsert(getUpdateFlagQuery())) {
184 LOGGER.debug("Migration finished for {} {}", item, uuid);
187 private void preparePersistMeasure(String uuid, Upsert update, AtomicReference<String> componentUuid, Map<String, Object> measureValues) throws SQLException {
188 LOGGER.debug("Persisting measures for component {}...", componentUuid.get());
189 String jsonValue = GSON.toJson(measureValues);
191 long jsonHash = MurmurHash3.hash128(jsonValue.getBytes(UTF_8))[0];
193 update.setString(1, componentUuid.get());
194 update.setString(2, uuid);
195 update.setString(3, jsonValue);
196 update.setLong(4, jsonHash);
197 update.setLong(5, system2.now());
198 update.setLong(6, system2.now());
201 private static void readMeasureValue(Select.Row row, Map<String, Object> measureValues) throws SQLException {
202 String metricName = row.getString(2);
203 String valueType = row.getString(3);
204 Double numericValue = row.getDouble(4);
205 String textValue = row.getString(5);
206 byte[] data = row.getBytes(6);
208 Object metricValue = getMetricValue(data, textValue, valueType, numericValue);
209 if (metricValue != null
210 && !measuresPlannedForDeletion(metricName)) {
211 measureValues.put(metricName, metricValue);
215 private static boolean measuresPlannedForDeletion(String metricName) {
216 return DeleteSoftwareQualityRatingFromProjectMeasures.SOFTWARE_QUALITY_METRICS_TO_DELETE.contains(metricName);
219 private static Object getMetricValue(@Nullable byte[] data, @Nullable String textValue, String valueType, Double numericValue) {
220 return TEXT_VALUE_TYPES.contains(valueType) ? getTextValue(data, textValue) : numericValue;
223 private static String getTextValue(@Nullable byte[] data, @Nullable String textValue) {
224 return data != null ? new String(data, UTF_8) : textValue;