Skip to content

Commit 52cf0c3

Browse files
authored
Fix duplicate key error on insertion (#13981)
* Fix duplicate key error on insertion * add test * fix empty lines * just throws * just throws
1 parent f205095 commit 52cf0c3

File tree

2 files changed

+118
-3
lines changed

2 files changed

+118
-3
lines changed

jablib/src/main/java/org/jabref/logic/search/indexing/BibFieldsIndexer.java

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -190,12 +190,17 @@ private void addToIndex(BibEntry bibEntry) {
190190
String insertFieldQuery = """
191191
INSERT INTO %s ("%s", "%s", "%s", "%s")
192192
VALUES (?, ?, ?, ?)
193+
ON CONFLICT ("%s", "%s")
194+
DO UPDATE SET "%s" = EXCLUDED."%s", "%s" = EXCLUDED."%s"
193195
""".formatted(
194196
schemaMainTableReference,
195197
ENTRY_ID,
196198
FIELD_NAME,
197199
FIELD_VALUE_LITERAL,
198-
FIELD_VALUE_TRANSFORMED);
200+
FIELD_VALUE_TRANSFORMED,
201+
ENTRY_ID, FIELD_NAME,
202+
FIELD_VALUE_LITERAL, FIELD_VALUE_LITERAL,
203+
FIELD_VALUE_TRANSFORMED, FIELD_VALUE_TRANSFORMED);
199204

200205
String insertIntoSplitTable = """
201206
INSERT INTO %s ("%s", "%s", "%s", "%s")
@@ -210,7 +215,7 @@ private void addToIndex(BibEntry bibEntry) {
210215
try (PreparedStatement preparedStatement = connection.prepareStatement(insertFieldQuery);
211216
PreparedStatement preparedStatementSplitValues = connection.prepareStatement(insertIntoSplitTable)) {
212217
String entryId = bibEntry.getId();
213-
LOGGER.atTrace().setMessage("Adding entry {}").addArgument(() -> bibEntry.getKeyAuthorTitleYear()).log();
218+
LOGGER.atTrace().setMessage("Adding entry {}").addArgument(bibEntry::getKeyAuthorTitleYear).log();
214219
for (Map.Entry<Field, String> fieldPair : bibEntry.getFieldMap().entrySet()) {
215220
Field field = fieldPair.getKey();
216221
String value = fieldPair.getValue();
@@ -342,7 +347,23 @@ ON CONFLICT ("%s", "%s")
342347
LOGGER.error("Could not add an entry to the index.", e);
343348
}
344349
} else {
345-
try (PreparedStatement preparedStatement = connection.prepareStatement(insertFieldQuery)) {
350+
// Use upsert for all non-date fields to avoid duplicate key errors when the same field is inserted multiple times quickly
351+
String upsertFieldQuery = """
352+
INSERT INTO %s ("%s", "%s", "%s", "%s")
353+
VALUES (?, ?, ?, ?)
354+
ON CONFLICT ("%s", "%s")
355+
DO UPDATE SET "%s" = EXCLUDED."%s", "%s" = EXCLUDED."%s"
356+
""".formatted(
357+
schemaMainTableReference,
358+
ENTRY_ID,
359+
FIELD_NAME,
360+
FIELD_VALUE_LITERAL,
361+
FIELD_VALUE_TRANSFORMED,
362+
ENTRY_ID, FIELD_NAME,
363+
FIELD_VALUE_LITERAL, FIELD_VALUE_LITERAL,
364+
FIELD_VALUE_TRANSFORMED, FIELD_VALUE_TRANSFORMED);
365+
366+
try (PreparedStatement preparedStatement = connection.prepareStatement(upsertFieldQuery)) {
346367
String value = entry.getField(field).orElse("");
347368

348369
Optional<String> resolvedFieldLatexFree = entry.getResolvedFieldOrAliasLatexFree(field, this.databaseContext.getDatabase());
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package org.jabref.logic.search.indexing;
2+
3+
import java.sql.Connection;
4+
import java.sql.PreparedStatement;
5+
import java.sql.ResultSet;
6+
import java.util.List;
7+
8+
import org.jabref.logic.search.PostgreServer;
9+
import org.jabref.logic.util.BackgroundTask;
10+
import org.jabref.model.database.BibDatabaseContext;
11+
import org.jabref.model.entry.BibEntry;
12+
import org.jabref.model.entry.BibEntryPreferences;
13+
import org.jabref.model.entry.field.StandardField;
14+
import org.jabref.model.entry.types.StandardEntryType;
15+
import org.jabref.model.search.PostgreConstants;
16+
17+
import org.junit.jupiter.api.AfterEach;
18+
import org.junit.jupiter.api.BeforeEach;
19+
import org.junit.jupiter.api.Test;
20+
21+
import static org.junit.jupiter.api.Assertions.assertEquals;
22+
import static org.mockito.Mockito.mock;
23+
import static org.mockito.Mockito.when;
24+
25+
public class BibFieldsIndexerTest {
26+
27+
private PostgreServer postgreServer;
28+
29+
private final BibEntryPreferences bibEntryPreferences = mock(BibEntryPreferences.class);
30+
31+
@BeforeEach
32+
void setUp() {
33+
when(bibEntryPreferences.getKeywordSeparator()).thenReturn(',');
34+
postgreServer = new PostgreServer();
35+
}
36+
37+
@AfterEach
38+
void tearDown() {
39+
if (postgreServer != null) {
40+
postgreServer.shutdown();
41+
}
42+
}
43+
44+
@Test
45+
void addToIndexIsIdempotentForSameEntry() throws Exception {
46+
BibDatabaseContext databaseContext = new BibDatabaseContext();
47+
Connection connection = postgreServer.getConnection();
48+
BibFieldsIndexer indexer = new BibFieldsIndexer(bibEntryPreferences, databaseContext, connection);
49+
50+
BibEntry entry = new BibEntry(StandardEntryType.Article);
51+
entry.withCitationKey("https://doi.org/10.48550/arxiv.2405.02318");
52+
entry.withField(StandardField.TITLE, "Cool Paper");
53+
entry.withField(StandardField.AUTHOR, "Doe, John");
54+
entry.withField(StandardField.GROUPS, "Imported entries, Other");
55+
56+
BackgroundTask<?> dummyTask = new BackgroundTask<>() {
57+
@Override
58+
public Object call() {
59+
return null;
60+
}
61+
};
62+
63+
// Index the same entry twice - this used to throw due to a duplicate key on (entryid, field_name)
64+
indexer.addToIndex(List.of(entry), dummyTask);
65+
indexer.addToIndex(List.of(entry), dummyTask);
66+
67+
// Verify that there's exactly one row for (entryid, 'citationkey')
68+
String tableRef = PostgreConstants.getMainTableSchemaReference(indexer.getTable());
69+
String sql = "SELECT COUNT(*) FROM " + tableRef + " WHERE \"" + PostgreConstants.ENTRY_ID + "\" = ? AND \"" + PostgreConstants.FIELD_NAME + "\" = ?";
70+
try (PreparedStatement ps = connection.prepareStatement(sql)) {
71+
ps.setString(1, entry.getId());
72+
ps.setString(2, "citationkey");
73+
try (ResultSet rs = ps.executeQuery()) {
74+
rs.next();
75+
int count = rs.getInt(1);
76+
assertEquals(1, count);
77+
}
78+
}
79+
80+
// Verify that there's exactly one row for (entryid, 'groups') in the main table as well
81+
try (PreparedStatement ps = connection.prepareStatement(sql)) {
82+
ps.setString(1, entry.getId());
83+
ps.setString(2, "groups");
84+
try (ResultSet rs = ps.executeQuery()) {
85+
rs.next();
86+
int count = rs.getInt(1);
87+
assertEquals(1, count);
88+
}
89+
}
90+
91+
// Cleanup resources gracefully
92+
indexer.closeAndWait();
93+
}
94+
}

0 commit comments

Comments
 (0)