Skip to content

Commit 0829bd1

Browse files
SLCORE-1762 Migrate Xodus known findings store to H2 DB
1 parent 3847e4a commit 0829bd1

File tree

7 files changed

+440
-25
lines changed

7 files changed

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

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 VARCHAR(255) 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)