Skip to content

Commit a48d701

Browse files
SLCORE-1762 Migrate Xodus known findings store to h2 db (#1552)
1 parent 3847e4a commit a48d701

File tree

7 files changed

+439
-25
lines changed

7 files changed

+439
-25
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* SonarLint Core - Commons
3+
* Copyright (C) 2016-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
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.
10+
*
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.
15+
*
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.
19+
*/
20+
package org.sonarsource.sonarlint.core.commons;
21+
22+
public enum KnownFindingType {
23+
ISSUE,
24+
HOTSPOT
25+
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/*
2+
* SonarLint Core - Commons
3+
* Copyright (C) 2016-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
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.
10+
*
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.
15+
*
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.
19+
*/
20+
package org.sonarsource.sonarlint.core.commons.storage.repository;
21+
22+
import java.nio.file.Path;
23+
import java.time.LocalDateTime;
24+
import java.time.ZoneId;
25+
import java.util.List;
26+
import org.jooq.Configuration;
27+
import org.jooq.Record;
28+
import org.sonarsource.sonarlint.core.commons.KnownFinding;
29+
import org.sonarsource.sonarlint.core.commons.KnownFindingType;
30+
import org.sonarsource.sonarlint.core.commons.LineWithHash;
31+
import org.sonarsource.sonarlint.core.commons.api.TextRangeWithHash;
32+
import org.sonarsource.sonarlint.core.commons.storage.SonarLintDatabase;
33+
34+
import static org.sonarsource.sonarlint.core.commons.storage.model.Tables.KNOWN_FINDINGS;
35+
36+
public class KnownFindingsRepository {
37+
38+
private final SonarLintDatabase database;
39+
40+
public KnownFindingsRepository(SonarLintDatabase database) {
41+
this.database = database;
42+
}
43+
44+
public void storeKnownIssues(String configurationScopeId, Path clientRelativePath, List<KnownFinding> newKnownIssues) {
45+
storeKnownFindings(configurationScopeId, clientRelativePath, newKnownIssues, KnownFindingType.ISSUE);
46+
}
47+
48+
public void storeKnownSecurityHotspots(String configurationScopeId, Path clientRelativePath, List<KnownFinding> newKnownSecurityHotspots) {
49+
storeKnownFindings(configurationScopeId, clientRelativePath, newKnownSecurityHotspots, KnownFindingType.HOTSPOT);
50+
}
51+
52+
public List<KnownFinding> loadSecurityHotspotsForFile(String configurationScopeId, Path filePath) {
53+
return getKnownFindingsForFile(configurationScopeId, filePath, KnownFindingType.HOTSPOT);
54+
}
55+
56+
public List<KnownFinding> loadIssuesForFile(String configurationScopeId, Path filePath) {
57+
return getKnownFindingsForFile(configurationScopeId, filePath, KnownFindingType.ISSUE);
58+
}
59+
60+
private void storeKnownFindings(String configurationScopeId, Path clientRelativePath, List<KnownFinding> newKnownFindings, KnownFindingType type) {
61+
database.dsl().transaction((Configuration trx) -> newKnownFindings.forEach(finding -> {
62+
var textRangeWithHash = finding.getTextRangeWithHash();
63+
var startLine = textRangeWithHash == null ? null : textRangeWithHash.getStartLine();
64+
var startLineOffset = textRangeWithHash == null ? null : textRangeWithHash.getStartLineOffset();
65+
var endLine = textRangeWithHash == null ? null : textRangeWithHash.getEndLine();
66+
var endLineOffset = textRangeWithHash == null ? null : textRangeWithHash.getEndLineOffset();
67+
var textRangeHash = textRangeWithHash == null ? null : textRangeWithHash.getHash();
68+
69+
var lineWithHash = finding.getLineWithHash();
70+
var line = lineWithHash == null ? null : lineWithHash.getNumber();
71+
var lineHash = lineWithHash == null ? null : lineWithHash.getHash();
72+
var introDate = LocalDateTime.ofInstant(finding.getIntroductionDate(), ZoneId.systemDefault());
73+
trx.dsl().mergeInto(KNOWN_FINDINGS)
74+
.using(trx.dsl().selectOne())
75+
.on(KNOWN_FINDINGS.ID.eq(finding.getId()))
76+
.whenMatchedThenUpdate()
77+
.set(KNOWN_FINDINGS.CONFIGURATION_SCOPE_ID, configurationScopeId)
78+
.set(KNOWN_FINDINGS.IDE_RELATIVE_FILE_PATH, clientRelativePath.toString())
79+
.set(KNOWN_FINDINGS.SERVER_KEY, finding.getServerKey())
80+
.set(KNOWN_FINDINGS.RULE_KEY, finding.getRuleKey())
81+
.set(KNOWN_FINDINGS.MESSAGE, finding.getMessage())
82+
.set(KNOWN_FINDINGS.INTRODUCTION_DATE, introDate)
83+
.set(KNOWN_FINDINGS.FINDING_TYPE, type.name())
84+
.set(KNOWN_FINDINGS.START_LINE, startLine)
85+
.set(KNOWN_FINDINGS.START_LINE_OFFSET, startLineOffset)
86+
.set(KNOWN_FINDINGS.END_LINE, endLine)
87+
.set(KNOWN_FINDINGS.END_LINE_OFFSET, endLineOffset)
88+
.set(KNOWN_FINDINGS.TEXT_RANGE_HASH, textRangeHash)
89+
.set(KNOWN_FINDINGS.LINE, line)
90+
.set(KNOWN_FINDINGS.LINE_HASH, lineHash)
91+
.whenNotMatchedThenInsert(KNOWN_FINDINGS.ID, KNOWN_FINDINGS.CONFIGURATION_SCOPE_ID, KNOWN_FINDINGS.IDE_RELATIVE_FILE_PATH, KNOWN_FINDINGS.SERVER_KEY,
92+
KNOWN_FINDINGS.RULE_KEY, KNOWN_FINDINGS.MESSAGE, KNOWN_FINDINGS.INTRODUCTION_DATE, KNOWN_FINDINGS.FINDING_TYPE,
93+
KNOWN_FINDINGS.START_LINE, KNOWN_FINDINGS.START_LINE_OFFSET, KNOWN_FINDINGS.END_LINE, KNOWN_FINDINGS.END_LINE_OFFSET, KNOWN_FINDINGS.TEXT_RANGE_HASH,
94+
KNOWN_FINDINGS.LINE, KNOWN_FINDINGS.LINE_HASH)
95+
.values(finding.getId(), configurationScopeId, clientRelativePath.toString(), finding.getServerKey(), finding.getRuleKey(),
96+
finding.getMessage(), introDate, type.name(),
97+
startLine, startLineOffset, endLine, endLineOffset, textRangeHash,
98+
line, lineHash
99+
)
100+
.execute();
101+
}));
102+
}
103+
104+
private List<KnownFinding> getKnownFindingsForFile(String configurationScopeId, Path filePath, KnownFindingType type) {
105+
var issuesInFile = database.dsl()
106+
.selectFrom(KNOWN_FINDINGS)
107+
.where(KNOWN_FINDINGS.CONFIGURATION_SCOPE_ID.eq(configurationScopeId)
108+
.and(KNOWN_FINDINGS.IDE_RELATIVE_FILE_PATH.eq(filePath.toString()))
109+
.and(KNOWN_FINDINGS.FINDING_TYPE.eq(type.name()))
110+
)
111+
.fetch();
112+
return issuesInFile.stream()
113+
.map(KnownFindingsRepository::recordToKnownFinding)
114+
.toList();
115+
}
116+
117+
private static KnownFinding recordToKnownFinding(Record rec) {
118+
var id = rec.get(KNOWN_FINDINGS.ID);
119+
var introductionDate = rec.get(KNOWN_FINDINGS.INTRODUCTION_DATE).atZone(ZoneId.systemDefault()).toInstant();
120+
var textRangeWithHash = getTextRangeWithHash(rec);
121+
var lineWithHash = getLineWithHash(rec);
122+
return new KnownFinding(
123+
id,
124+
rec.get(KNOWN_FINDINGS.SERVER_KEY),
125+
textRangeWithHash, lineWithHash,
126+
rec.get(KNOWN_FINDINGS.RULE_KEY),
127+
rec.get(KNOWN_FINDINGS.MESSAGE),
128+
introductionDate
129+
);
130+
}
131+
132+
private static LineWithHash getLineWithHash(Record rec) {
133+
if (rec.get(KNOWN_FINDINGS.LINE) == null) return null;
134+
var line = rec.get(KNOWN_FINDINGS.LINE);
135+
var hash = rec.get(KNOWN_FINDINGS.LINE_HASH);
136+
return new LineWithHash(line, hash);
137+
}
138+
139+
private static TextRangeWithHash getTextRangeWithHash(Record rec) {
140+
if (rec.get(KNOWN_FINDINGS.START_LINE) == null) return null;
141+
var startLine = rec.get(KNOWN_FINDINGS.START_LINE);
142+
var endLine = rec.get(KNOWN_FINDINGS.END_LINE);
143+
var startLineOffset = rec.get(KNOWN_FINDINGS.START_LINE_OFFSET);
144+
var endLineOffset = rec.get(KNOWN_FINDINGS.END_LINE_OFFSET);
145+
var hash = rec.get(KNOWN_FINDINGS.TEXT_RANGE_HASH);
146+
return new TextRangeWithHash(startLine, startLineOffset, endLine, endLineOffset, hash);
147+
}
148+
149+
}

backend/commons/src/main/resources/db/migration/V1__create_ai_codefix_settings_table.sql

Lines changed: 0 additions & 12 deletions
This file was deleted.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
-- Flyway migration: create AI_CODEFIX_SETTINGS table for H2
2+
-- Initial schema includes per-connection scoping via connection_id
3+
CREATE TABLE IF NOT EXISTS AI_CODEFIX_SETTINGS (
4+
connection_id VARCHAR(255) NOT NULL PRIMARY KEY,
5+
supported_rules VARCHAR(200) ARRAY,
6+
organization_eligible BOOLEAN,
7+
enablement VARCHAR(64),
8+
enabled_project_keys VARCHAR(400) ARRAY,
9+
CONSTRAINT pk_ai_codefix_settings PRIMARY KEY (connection_id)
10+
);
11+
12+
CREATE TABLE IF NOT EXISTS KNOWN_FINDINGS (
13+
-- UUID
14+
id UUID NOT NULL PRIMARY KEY,
15+
configuration_scope_id VARCHAR(255) NOT NULL,
16+
ide_relative_file_path VARCHAR(255) NOT NULL,
17+
server_key VARCHAR(255),
18+
rule_key VARCHAR(255) NOT NULL,
19+
message VARCHAR(255) NOT NULL,
20+
introduction_date TIMESTAMP NOT NULL,
21+
finding_type VARCHAR(255) NOT NULL,
22+
-- TextRangeWithHash
23+
start_line INT,
24+
start_line_offset INT,
25+
end_line INT,
26+
end_line_offset INT,
27+
text_range_hash VARCHAR(255),
28+
-- LineWithHash
29+
line INT,
30+
line_hash VARCHAR(255)
31+
);
32+
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* SonarLint Core - Commons
3+
* Copyright (C) 2016-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
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.
10+
*
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.
15+
*
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.
19+
*/
20+
package org.sonarsource.sonarlint.core.commons.storage.repository;
21+
22+
import static org.assertj.core.api.Assertions.assertThat;
23+
24+
import java.nio.file.Path;
25+
import java.util.Set;
26+
import org.junit.jupiter.api.Test;
27+
import org.junit.jupiter.api.io.TempDir;
28+
import org.junit.jupiter.api.extension.RegisterExtension;
29+
import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester;
30+
import org.sonarsource.sonarlint.core.commons.storage.SonarLintDatabaseMode;
31+
import org.sonarsource.sonarlint.core.commons.storage.SonarLintDatabase;
32+
import org.sonarsource.sonarlint.core.commons.storage.SonarLintDatabaseInitParams;
33+
import org.sonarsource.sonarlint.core.commons.storage.model.AiCodeFix;
34+
35+
class AiCodeFixRepositoryTests {
36+
37+
@RegisterExtension
38+
static SonarLintLogTester logTester = new SonarLintLogTester();
39+
40+
@TempDir
41+
Path temp;
42+
43+
@Test
44+
void upsert_and_get_should_persist_to_h2_file_database() {
45+
// Given a file-based H2 database under a temporary storage root
46+
var storageRoot = temp.resolve("storage");
47+
48+
var db = new SonarLintDatabase(new SonarLintDatabaseInitParams(storageRoot, SonarLintDatabaseMode.FILE));
49+
var aiCodeFixRepo = new AiCodeFixRepository(db);
50+
51+
var entityToStore = new AiCodeFix(
52+
"test-connection",
53+
Set.of("java:S100", "js:S200"),
54+
true,
55+
AiCodeFix.Enablement.ENABLED_FOR_SOME_PROJECTS,
56+
Set.of("project-a", "project-b")
57+
);
58+
59+
// When we upsert the entity
60+
aiCodeFixRepo.upsert(entityToStore);
61+
62+
// And shutdown the first DB to force closing connections
63+
db.shutdown();
64+
65+
// Create a new repository with a fresh DB instance pointing to the same storage root
66+
var db2 = new SonarLintDatabase(new SonarLintDatabaseInitParams(storageRoot, SonarLintDatabaseMode.FILE));
67+
var repo2 = new AiCodeFixRepository(db2);
68+
// With a different connection id, no settings should be visible
69+
var loadedOptDifferent = repo2.get("test-connection-2");
70+
assertThat(loadedOptDifferent).isEmpty();
71+
72+
// With the same connection id, we should read back exactly what we stored
73+
var repoSame = new AiCodeFixRepository(db2);
74+
var loadedOpt = repoSame.get("test-connection");
75+
assertThat(loadedOpt).isPresent();
76+
var loaded = loadedOpt.get();
77+
78+
assertThat(loaded.supportedRules()).containsExactlyInAnyOrder("java:S100", "js:S200");
79+
assertThat(loaded.organizationEligible()).isTrue();
80+
assertThat(loaded.enablement()).isEqualTo(AiCodeFix.Enablement.ENABLED_FOR_SOME_PROJECTS);
81+
assertThat(loaded.enabledProjectKeys()).containsExactlyInAnyOrder("project-a", "project-b");
82+
83+
db2.shutdown();
84+
}
85+
}

0 commit comments

Comments
 (0)