@@ -244,6 +244,9 @@ public class AbstractDbTester<T extends CoreTestDb> extends ExternalResource { | |||
} else if (value instanceof Integer) { | |||
// To be consistent, all INTEGER types are mapped as Long | |||
value = ((Integer) value).longValue(); | |||
} else if (value instanceof Byte) { | |||
Byte byteValue = (Byte) value; | |||
value = byteValue.intValue(); | |||
} else if (value instanceof Timestamp) { | |||
value = new Date(((Timestamp) value).getTime()); | |||
} |
@@ -31,6 +31,7 @@ public class DbVersion78 implements DbVersion { | |||
.add(2701, "Add index to org_qprofile.parent_uuid", AddIndexToOrgQProfileParentUuid.class) | |||
.add(2702, "Add column webhooks.secret", AddWebhooksSecret.class) | |||
.add(2703, "Add security fields to Elasticsearch indices", AddSecurityFieldsToElasticsearchIndices.class) | |||
.add(2704, "Add InternalComponentProperties table", CreateInternalComponentPropertiesTable.class); | |||
.add(2704, "Add InternalComponentProperties table", CreateInternalComponentPropertiesTable.class) | |||
.add(2707, "Update statuses of Security Hotspots", UpdateSecurityHotspotsStatuses.class); | |||
} | |||
} |
@@ -0,0 +1,242 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2019 SonarSource SA | |||
* mailto:info 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.server.platform.db.migration.version.v78; | |||
import com.google.common.collect.Maps; | |||
import java.io.Serializable; | |||
import java.sql.SQLException; | |||
import java.util.Map; | |||
import java.util.Objects; | |||
import javax.annotation.CheckForNull; | |||
import javax.annotation.Nullable; | |||
import org.sonar.api.config.Configuration; | |||
import org.sonar.api.utils.System2; | |||
import org.sonar.core.util.UuidFactory; | |||
import org.sonar.db.Database; | |||
import org.sonar.server.platform.db.migration.SupportsBlueGreen; | |||
import org.sonar.server.platform.db.migration.es.MigrationEsClient; | |||
import org.sonar.server.platform.db.migration.step.DataChange; | |||
import org.sonar.server.platform.db.migration.step.MassUpdate; | |||
@SupportsBlueGreen | |||
public class UpdateSecurityHotspotsStatuses extends DataChange { | |||
private static final String RESOLUTION_FIXED = "FIXED"; | |||
private static final String RESOLUTION_WONT_FIX = "WONTFIX"; | |||
private static final String STATUS_OPEN = "OPEN"; | |||
private static final String STATUS_REOPENED = "REOPENED"; | |||
private static final String STATUS_RESOLVED = "RESOLVED"; | |||
private static final String STATUS_TO_REVIEW = "TOREVIEW"; | |||
private static final String STATUS_IN_REVIEW = "INREVIEW"; | |||
private static final String STATUS_REVIEWED = "REVIEWED"; | |||
private static final int RULE_TYPE_SECURITY_HOTSPOT = 4; | |||
private final Configuration configuration; | |||
private final System2 system2; | |||
private final MigrationEsClient esClient; | |||
private final UuidFactory uuidFactory; | |||
public UpdateSecurityHotspotsStatuses(Database db, Configuration configuration, System2 system2, MigrationEsClient esClient, UuidFactory uuidFactory) { | |||
super(db); | |||
this.configuration = configuration; | |||
this.system2 = system2; | |||
this.esClient = esClient; | |||
this.uuidFactory = uuidFactory; | |||
} | |||
@Override | |||
public void execute(Context context) throws SQLException { | |||
if (configuration.getBoolean("sonar.sonarcloud.enabled").orElse(false)) { | |||
return; | |||
} | |||
long now = system2.now(); | |||
MassUpdate massUpdate = context.prepareMassUpdate().rowPluralName("security hotspots"); | |||
massUpdate.select("select i.kee, i.status, i.resolution, i.issue_type from issues i " + | |||
"inner join rules r on r.id = i.rule_id and r.rule_type = ? " + | |||
"where (i.resolution is null or i.resolution in (?, ?)) and i.issue_type=? " + | |||
// Add status check for the re-entrance, in order to not reload already migrated issues | |||
"and i.status not in (?, ?, ?)") | |||
.setInt(1, RULE_TYPE_SECURITY_HOTSPOT) | |||
.setString(2, RESOLUTION_FIXED) | |||
.setString(3, RESOLUTION_WONT_FIX) | |||
.setInt(4, RULE_TYPE_SECURITY_HOTSPOT) | |||
.setString(5, STATUS_TO_REVIEW) | |||
.setString(6, STATUS_IN_REVIEW) | |||
.setString(7, STATUS_REVIEWED); | |||
massUpdate.update("update issues set status=?, resolution=?, updated_at=? where kee=? "); | |||
massUpdate.update("insert into issue_changes (kee, issue_key, change_type, change_data, created_at, updated_at, issue_change_creation_date) values (?, ?, 'diff', ?, ?, ?, ?)"); | |||
massUpdate.execute((row, update, updateIndex) -> { | |||
String issueKey = row.getString(1); | |||
String status = row.getString(2); | |||
String resolution = row.getNullableString(3); | |||
IssueUpdate issueUpdate = new IssueUpdate(status, resolution); | |||
FieldDiffs fieldDiffs = issueUpdate.process(); | |||
if (!issueUpdate.isUpdated()) { | |||
return false; | |||
} | |||
if (updateIndex == 0) { | |||
update.setString(1, issueUpdate.getNewStatus()); | |||
update.setString(2, issueUpdate.getNewResolution()); | |||
update.setLong(3, now); | |||
update.setString(4, issueKey); | |||
return true; | |||
} else { | |||
// No changelog on OPEN issue as there was no previous state | |||
if (!status.equals(STATUS_OPEN)) { | |||
update.setString(1, uuidFactory.create()); | |||
update.setString(2, issueKey); | |||
update.setString(3, fieldDiffs.toEncodedString()); | |||
update.setLong(4, now); | |||
update.setLong(5, now); | |||
update.setLong(6, now); | |||
return true; | |||
} | |||
return false; | |||
} | |||
}); | |||
esClient.deleteIndexes("issues"); | |||
} | |||
private static class IssueUpdate { | |||
private static final String RESOLUTION_FIELD = "resolution"; | |||
private static final String STATUS_FIELD = "status"; | |||
private final String status; | |||
private final String resolution; | |||
private String newStatus; | |||
private String newResolution; | |||
private boolean updated; | |||
IssueUpdate(String status, @CheckForNull String resolution) { | |||
this.status = status; | |||
this.resolution = resolution; | |||
} | |||
FieldDiffs process() { | |||
if ((status.equals(STATUS_OPEN) || (status.equals(STATUS_REOPENED))) && resolution == null) { | |||
newStatus = STATUS_TO_REVIEW; | |||
newResolution = null; | |||
updated = true; | |||
} else if (status.equals(STATUS_RESOLVED) && resolution != null) { | |||
if (resolution.equals(RESOLUTION_FIXED)) { | |||
newStatus = STATUS_IN_REVIEW; | |||
newResolution = null; | |||
updated = true; | |||
} else if (resolution.equals(RESOLUTION_WONT_FIX)) { | |||
newStatus = STATUS_REVIEWED; | |||
newResolution = RESOLUTION_FIXED; | |||
updated = true; | |||
} | |||
} | |||
FieldDiffs fieldDiffs = new FieldDiffs(); | |||
fieldDiffs.setDiff(STATUS_FIELD, status, newStatus); | |||
fieldDiffs.setDiff(RESOLUTION_FIELD, resolution, newResolution); | |||
return fieldDiffs; | |||
} | |||
String getNewStatus() { | |||
return newStatus; | |||
} | |||
String getNewResolution() { | |||
return newResolution; | |||
} | |||
boolean isUpdated() { | |||
return updated; | |||
} | |||
} | |||
/** | |||
* Inspired and simplified from {@link org.sonar.core.issue.FieldDiffs} | |||
*/ | |||
static class FieldDiffs implements Serializable { | |||
private final Map<String, Diff> diffs = Maps.newLinkedHashMap(); | |||
void setDiff(String field, @Nullable String oldValue, @Nullable String newValue) { | |||
diffs.put(field, new Diff(oldValue, newValue)); | |||
} | |||
String toEncodedString() { | |||
StringBuilder sb = new StringBuilder(); | |||
boolean notFirst = false; | |||
for (Map.Entry<String, FieldDiffs.Diff> entry : diffs.entrySet()) { | |||
if (notFirst) { | |||
sb.append(','); | |||
} else { | |||
notFirst = true; | |||
} | |||
sb.append(entry.getKey()); | |||
sb.append('='); | |||
sb.append(entry.getValue().toEncodedString()); | |||
} | |||
return sb.toString(); | |||
} | |||
static class Diff implements Serializable { | |||
private String oldValue; | |||
private String newValue; | |||
Diff(@Nullable String oldValue, @Nullable String newValue) { | |||
this.oldValue = oldValue; | |||
this.newValue = newValue; | |||
} | |||
private String toEncodedString() { | |||
StringBuilder sb = new StringBuilder(); | |||
if (oldValue != null) { | |||
sb.append(oldValue); | |||
sb.append('|'); | |||
} | |||
if (newValue != null) { | |||
sb.append(newValue); | |||
} | |||
return sb.toString(); | |||
} | |||
@Override | |||
public boolean equals(Object o) { | |||
if (this == o) { | |||
return true; | |||
} | |||
if (o == null || getClass() != o.getClass()) { | |||
return false; | |||
} | |||
FieldDiffs.Diff diff = (FieldDiffs.Diff) o; | |||
return Objects.equals(oldValue, diff.oldValue) && | |||
Objects.equals(newValue, diff.newValue); | |||
} | |||
@Override | |||
public int hashCode() { | |||
return Objects.hash(oldValue, newValue); | |||
} | |||
} | |||
} | |||
} |
@@ -35,7 +35,7 @@ public class DbVersion78Test { | |||
@Test | |||
public void verify_migration_count() { | |||
verifyMigrationCount(underTest, 5); | |||
verifyMigrationCount(underTest, 6); | |||
} | |||
} |
@@ -0,0 +1,233 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2019 SonarSource SA | |||
* mailto:info 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.server.platform.db.migration.version.v78; | |||
import java.sql.SQLException; | |||
import javax.annotation.Nullable; | |||
import org.apache.commons.lang.math.RandomUtils; | |||
import org.assertj.core.groups.Tuple; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.sonar.api.config.internal.MapSettings; | |||
import org.sonar.api.utils.internal.TestSystem2; | |||
import org.sonar.core.util.UuidFactoryFast; | |||
import org.sonar.db.CoreDbTester; | |||
import org.sonar.server.platform.db.migration.es.MigrationEsClient; | |||
import org.sonar.server.platform.db.migration.step.DataChange; | |||
import static java.util.stream.Collectors.toList; | |||
import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic; | |||
import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.assertj.core.api.Assertions.tuple; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.verify; | |||
public class UpdateSecurityHotspotsStatusesTest { | |||
private static final long PAST = 5_000_000_000L; | |||
private static final long NOW = 10_000_000_000L; | |||
@Rule | |||
public CoreDbTester db = CoreDbTester.createForSchema(UpdateSecurityHotspotsStatusesTest.class, "schema.sql"); | |||
private MapSettings settings = new MapSettings(); | |||
private TestSystem2 system2 = new TestSystem2().setNow(NOW); | |||
private MigrationEsClient esClient = mock(MigrationEsClient.class); | |||
private DataChange underTest = new UpdateSecurityHotspotsStatuses(db.database(), settings.asConfig(), system2, esClient, UuidFactoryFast.getInstance()); | |||
@Test | |||
public void migrate_open_and_reopen_hotspots() throws SQLException { | |||
int rule = insertRule(4); | |||
String issue1 = insertIssue("OPEN", null, 4, rule); | |||
String issue2 = insertIssue("REOPENED", null, 4, rule); | |||
// Other type of issues should not be updated | |||
String issue3 = insertIssue("OPEN", null, 1, rule); | |||
String issue4 = insertIssue("REOPENED", null, 2, rule); | |||
String issue5 = insertIssue("OPEN", null, 3, rule); | |||
underTest.execute(); | |||
assertIssues( | |||
tuple(issue1, "TOREVIEW", null, 4, NOW), | |||
tuple(issue2, "TOREVIEW", null, 4, NOW), | |||
// Not updated | |||
tuple(issue3, "OPEN", null, 1, PAST), | |||
tuple(issue4, "REOPENED", null, 2, PAST), | |||
tuple(issue5, "OPEN", null, 3, PAST)); | |||
} | |||
@Test | |||
public void migrate_resolved_as_fixed_and_wont_fix_hotspots() throws SQLException { | |||
int rule = insertRule(4); | |||
String issue1 = insertIssue("RESOLVED", "FIXED", 4, rule); | |||
String issue2 = insertIssue("RESOLVED", "WONTFIX", 4, rule); | |||
// Other type of issues should not be updated | |||
String issue3 = insertIssue("RESOLVED", "FIXED", 1, rule); | |||
String issue4 = insertIssue("RESOLVED", "WONTFIX", 2, rule); | |||
String issue5 = insertIssue("RESOLVED", "WONTFIX", 3, rule); | |||
underTest.execute(); | |||
assertIssues( | |||
tuple(issue1, "INREVIEW", null, 4, NOW), | |||
tuple(issue2, "REVIEWED", "FIXED", 4, NOW), | |||
// Not updated | |||
tuple(issue3, "RESOLVED", "FIXED", 1, PAST), | |||
tuple(issue4, "RESOLVED", "WONTFIX", 2, PAST), | |||
tuple(issue5, "RESOLVED", "WONTFIX", 3, PAST)); | |||
} | |||
@Test | |||
public void insert_issue_changes() throws SQLException { | |||
int rule = insertRule(4); | |||
String issue1 = insertIssue("REOPENED", null, 4, rule); | |||
// No changelog on OPEN issue as there was no previous state | |||
String issue2 = insertIssue("OPEN", null, 4, rule); | |||
String issue3 = insertIssue("RESOLVED", "FIXED", 4, rule); | |||
String issue4 = insertIssue("RESOLVED", "WONTFIX", 4, rule); | |||
underTest.execute(); | |||
assertIssueChanges( | |||
tuple(issue1, "diff", "status=REOPENED|TOREVIEW,resolution=", NOW, NOW, NOW), | |||
tuple(issue3, "diff", "status=RESOLVED|INREVIEW,resolution=FIXED|", NOW, NOW, NOW), | |||
tuple(issue4, "diff", "status=RESOLVED|REVIEWED,resolution=WONTFIX|FIXED", NOW, NOW, NOW)); | |||
} | |||
@Test | |||
public void do_not_update_vulnerabilities_coming_from_hotspot() throws SQLException { | |||
int rule = insertRule(4); | |||
String issue1 = insertIssue("OPEN", null, 3, rule); | |||
underTest.execute(); | |||
assertIssues(tuple(issue1, "OPEN", null, 3, PAST)); | |||
assertNoIssueChanges(); | |||
} | |||
@Test | |||
public void do_not_update_closed_hotspots() throws SQLException { | |||
int rule = insertRule(4); | |||
String issue1 = insertIssue("CLOSED", "FIXED", 4, rule); | |||
String issue2 = insertIssue("CLOSED", "REMOVED", 4, rule); | |||
underTest.execute(); | |||
assertIssues( | |||
tuple(issue1, "CLOSED", "FIXED", 4, PAST), | |||
tuple(issue2, "CLOSED", "REMOVED", 4, PAST)); | |||
assertNoIssueChanges(); | |||
} | |||
@Test | |||
public void do_nothing_on_sonarcloud() throws SQLException { | |||
settings.setProperty("sonar.sonarcloud.enabled", "true"); | |||
int rule = insertRule(4); | |||
String issue1 = insertIssue("OPEN", null, 4, rule); | |||
underTest.execute(); | |||
assertIssues(tuple(issue1, "OPEN", null, 4, PAST)); | |||
assertNoIssueChanges(); | |||
} | |||
@Test | |||
public void migration_is_reentrant() throws SQLException { | |||
int rule = insertRule(4); | |||
String issue1 = insertIssue("OPEN", null, 4, rule); | |||
String issue2 = insertIssue("REOPENED", null, 4, rule); | |||
underTest.execute(); | |||
assertIssues( | |||
tuple(issue1, "TOREVIEW", null, 4, NOW), | |||
tuple(issue2, "TOREVIEW", null, 4, NOW)); | |||
// Set a new date for NOW in order to check that issues has not been updated again | |||
system2.setNow(NOW + 1_000_000_000L); | |||
underTest.execute(); | |||
assertIssues( | |||
tuple(issue1, "TOREVIEW", null, 4, NOW), | |||
tuple(issue2, "TOREVIEW", null, 4, NOW)); | |||
} | |||
@Test | |||
public void issues_index_is_removed() throws SQLException { | |||
underTest.execute(); | |||
verify(esClient).deleteIndexes("issues"); | |||
} | |||
private void assertIssues(Tuple... expectedTuples) { | |||
assertThat(db.select("SELECT kee, status, resolution, issue_type, updated_at FROM issues") | |||
.stream() | |||
.map(map -> new Tuple(map.get("KEE"), map.get("STATUS"), map.get("RESOLUTION"), map.get("ISSUE_TYPE"), map.get("UPDATED_AT"))) | |||
.collect(toList())) | |||
.containsExactlyInAnyOrder(expectedTuples); | |||
} | |||
private void assertNoIssueChanges() { | |||
assertThat(db.countRowsOfTable("issue_changes")).isZero(); | |||
} | |||
private void assertIssueChanges(Tuple... expectedTuples) { | |||
assertThat(db.select("SELECT issue_key, change_type, change_data, created_at, updated_at, issue_change_creation_date FROM issue_changes") | |||
.stream() | |||
.map(map -> new Tuple(map.get("ISSUE_KEY"), map.get("CHANGE_TYPE"), map.get("CHANGE_DATA"), map.get("CREATED_AT"), map.get("UPDATED_AT"), | |||
map.get("ISSUE_CHANGE_CREATION_DATE"))) | |||
.collect(toList())) | |||
.containsExactlyInAnyOrder(expectedTuples); | |||
} | |||
private String insertIssue(String status, @Nullable String resolution, int issueType, int ruleId) { | |||
String issueKey = randomAlphabetic(3); | |||
db.executeInsert( | |||
"ISSUES", | |||
"KEE", issueKey, | |||
"STATUS", status, | |||
"RESOLUTION", resolution, | |||
"RULE_ID", ruleId, | |||
"ISSUE_TYPE", issueType, | |||
"COMPONENT_UUID", randomAlphanumeric(10), | |||
"PROJECT_UUID", randomAlphanumeric(10), | |||
"MANUAL_SEVERITY", false, | |||
"CREATED_AT", PAST, | |||
"UPDATED_AT", PAST); | |||
return issueKey; | |||
} | |||
private int insertRule(int ruleType) { | |||
int id = RandomUtils.nextInt(); | |||
db.executeInsert("RULES", | |||
"ID", id, | |||
"RULE_TYPE", ruleType, | |||
"IS_EXTERNAL", false, | |||
"PLUGIN_RULE_KEY", randomAlphanumeric(3), | |||
"PLUGIN_NAME", randomAlphanumeric(3), | |||
"SCOPE", "MAIN", | |||
"IS_AD_HOC", false, | |||
"CREATED_AT", PAST, | |||
"UPDATED_AT", PAST); | |||
return id; | |||
} | |||
} |
@@ -0,0 +1,81 @@ | |||
CREATE TABLE "ISSUES" ( | |||
"ID" BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1), | |||
"KEE" VARCHAR(50) UNIQUE NOT NULL, | |||
"COMPONENT_UUID" VARCHAR(50), | |||
"PROJECT_UUID" VARCHAR(50), | |||
"RULE_ID" INTEGER, | |||
"SEVERITY" VARCHAR(10), | |||
"MANUAL_SEVERITY" BOOLEAN NOT NULL, | |||
"MESSAGE" VARCHAR(4000), | |||
"LINE" INTEGER, | |||
"GAP" DOUBLE, | |||
"EFFORT" INTEGER, | |||
"STATUS" VARCHAR(20), | |||
"RESOLUTION" VARCHAR(20), | |||
"CHECKSUM" VARCHAR(1000), | |||
"REPORTER" VARCHAR(255), | |||
"ASSIGNEE" VARCHAR(255), | |||
"AUTHOR_LOGIN" VARCHAR(255), | |||
"ACTION_PLAN_KEY" VARCHAR(50) NULL, | |||
"ISSUE_ATTRIBUTES" VARCHAR(4000), | |||
"TAGS" VARCHAR(4000), | |||
"ISSUE_CREATION_DATE" BIGINT, | |||
"ISSUE_CLOSE_DATE" BIGINT, | |||
"ISSUE_UPDATE_DATE" BIGINT, | |||
"CREATED_AT" BIGINT, | |||
"UPDATED_AT" BIGINT, | |||
"LOCATIONS" BLOB, | |||
"ISSUE_TYPE" TINYINT, | |||
"FROM_HOTSPOT" BOOLEAN NULL | |||
); | |||
CREATE UNIQUE INDEX "ISSUES_KEE" ON "ISSUES" ("KEE"); | |||
CREATE INDEX "ISSUES_COMPONENT_UUID" ON "ISSUES" ("COMPONENT_UUID"); | |||
CREATE INDEX "ISSUES_PROJECT_UUID" ON "ISSUES" ("PROJECT_UUID"); | |||
CREATE INDEX "ISSUES_RULE_ID" ON "ISSUES" ("RULE_ID"); | |||
CREATE INDEX "ISSUES_RESOLUTION" ON "ISSUES" ("RESOLUTION"); | |||
CREATE INDEX "ISSUES_ASSIGNEE" ON "ISSUES" ("ASSIGNEE"); | |||
CREATE INDEX "ISSUES_CREATION_DATE" ON "ISSUES" ("ISSUE_CREATION_DATE"); | |||
CREATE INDEX "ISSUES_UPDATED_AT" ON "ISSUES" ("UPDATED_AT"); | |||
CREATE TABLE "ISSUE_CHANGES" ( | |||
"ID" BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1), | |||
"KEE" VARCHAR(50), | |||
"ISSUE_KEY" VARCHAR(50) NOT NULL, | |||
"USER_LOGIN" VARCHAR(255), | |||
"CHANGE_TYPE" VARCHAR(40), | |||
"CHANGE_DATA" VARCHAR(16777215), | |||
"CREATED_AT" BIGINT, | |||
"UPDATED_AT" BIGINT, | |||
"ISSUE_CHANGE_CREATION_DATE" BIGINT | |||
); | |||
CREATE INDEX "ISSUE_CHANGES_KEE" ON "ISSUE_CHANGES" ("KEE"); | |||
CREATE INDEX "ISSUE_CHANGES_ISSUE_KEY" ON "ISSUE_CHANGES" ("ISSUE_KEY"); | |||
CREATE TABLE "RULES" ( | |||
"ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1), | |||
"PLUGIN_KEY" VARCHAR(200), | |||
"PLUGIN_RULE_KEY" VARCHAR(200) NOT NULL, | |||
"PLUGIN_NAME" VARCHAR(255) NOT NULL, | |||
"DESCRIPTION" VARCHAR(16777215), | |||
"DESCRIPTION_FORMAT" VARCHAR(20), | |||
"PRIORITY" INTEGER, | |||
"IS_TEMPLATE" BOOLEAN DEFAULT FALSE, | |||
"IS_EXTERNAL" BOOLEAN NOT NULL, | |||
"IS_AD_HOC" BOOLEAN NOT NULL, | |||
"TEMPLATE_ID" INTEGER, | |||
"PLUGIN_CONFIG_KEY" VARCHAR(200), | |||
"NAME" VARCHAR(200), | |||
"STATUS" VARCHAR(40), | |||
"LANGUAGE" VARCHAR(20), | |||
"SCOPE" VARCHAR(20) NOT NULL, | |||
"DEF_REMEDIATION_FUNCTION" VARCHAR(20), | |||
"DEF_REMEDIATION_GAP_MULT" VARCHAR(20), | |||
"DEF_REMEDIATION_BASE_EFFORT" VARCHAR(20), | |||
"GAP_DESCRIPTION" VARCHAR(4000), | |||
"SYSTEM_TAGS" VARCHAR(4000), | |||
"SECURITY_STANDARDS" VARCHAR(4000), | |||
"RULE_TYPE" TINYINT, | |||
"CREATED_AT" BIGINT, | |||
"UPDATED_AT" BIGINT | |||
); | |||
CREATE UNIQUE INDEX "RULES_REPO_KEY" ON "RULES" ("PLUGIN_NAME", "PLUGIN_RULE_KEY"); |
@@ -204,7 +204,7 @@ public class WebIssueStorageTest { | |||
.containsEntry("COMPONENT_UUID", issue.componentUuid()) | |||
.containsEntry("EFFORT", updated.effortInMinutes()) | |||
.containsEntry("ISSUE_ATTRIBUTES", "fox=bax") | |||
.containsEntry("ISSUE_TYPE", (byte) 3) | |||
.containsEntry("ISSUE_TYPE", 3) | |||
.containsEntry("KEE", issue.key()) | |||
.containsEntry("LINE", (long) updated.line()) | |||
.containsEntry("PROJECT_UUID", updated.projectUuid()) |