Skip to content

Commit 8043b6d

Browse files
authored
GH-5590 add isolation level selection for console load (#5598)
2 parents 8b72792 + 7986900 commit 8043b6d

2 files changed

Lines changed: 143 additions & 27 deletions

File tree

tools/console/src/main/java/org/eclipse/rdf4j/console/command/Load.java

Lines changed: 75 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
import java.nio.file.Path;
1818
import java.util.Map;
1919

20+
import org.eclipse.rdf4j.common.transaction.IsolationLevel;
21+
import org.eclipse.rdf4j.common.transaction.IsolationLevels;
2022
import org.eclipse.rdf4j.console.ConsoleIO;
2123
import org.eclipse.rdf4j.console.ConsoleState;
2224
import org.eclipse.rdf4j.console.LockRemover;
@@ -49,10 +51,11 @@ public String getHelpShort() {
4951

5052
@Override
5153
public String getHelpLong() {
52-
return PrintHelp.USAGE + "load <file-or-url> [from <base-uri>] [into <context-id>]\n"
54+
return PrintHelp.USAGE + "load <file-or-url> [from <base-uri>] [into <context-id>] [isolation <level>]\n"
5355
+ " <file-or-url> The path or URL identifying the data file\n"
5456
+ " <base-uri> The base URI to use for resolving relative references, defaults to <file-or-url>\n"
5557
+ " <context-id> The ID of the context to add the data to, e.g. foo:bar or _:n123\n"
58+
+ " <level> Isolation level to use when loading data (defaults to NONE)\n"
5659
+ "Loads the specified data file into the current repository\n";
5760
}
5861

@@ -83,21 +86,43 @@ public void execute(final String... tokens) {
8386
} else {
8487
String baseURI = null;
8588
String context = null;
89+
IsolationLevel isolationLevel = null;
8690

8791
int index = 2;
88-
if (tokens.length >= index + 2 && tokens[index].equalsIgnoreCase("from")) {
89-
baseURI = tokens[index + 1];
90-
index += 2;
91-
}
92-
if (tokens.length >= index + 2 && tokens[index].equalsIgnoreCase("into")) {
93-
context = tokens[tokens.length - 1];
94-
index += 2;
95-
}
96-
if (index < tokens.length) {
97-
writeln(getHelpLong());
98-
} else {
99-
load(repository, baseURI, context, tokens);
92+
while (index < tokens.length) {
93+
if (tokens[index].equalsIgnoreCase("from")) {
94+
if (tokens.length < index + 2) {
95+
writeln(getHelpLong());
96+
return;
97+
}
98+
baseURI = tokens[index + 1];
99+
index += 2;
100+
} else if (tokens[index].equalsIgnoreCase("into")) {
101+
if (tokens.length < index + 2) {
102+
writeln(getHelpLong());
103+
return;
104+
}
105+
context = tokens[index + 1];
106+
index += 2;
107+
} else if (tokens[index].equalsIgnoreCase("isolation")) {
108+
if (tokens.length < index + 2) {
109+
writeln(getHelpLong());
110+
return;
111+
}
112+
try {
113+
isolationLevel = IsolationLevels.valueOf(tokens[index + 1].toUpperCase());
114+
} catch (IllegalArgumentException e) {
115+
writeError("Unknown isolation level: " + tokens[index + 1]);
116+
return;
117+
}
118+
index += 2;
119+
} else {
120+
writeln(getHelpLong());
121+
return;
122+
}
100123
}
124+
125+
load(repository, baseURI, context, isolationLevel, tokens);
101126
}
102127
}
103128
}
@@ -114,12 +139,14 @@ private Path getWorkDir() {
114139
/**
115140
* Load data into a repository
116141
*
117-
* @param repository repository
142+
* @param repository repository
118143
* @param baseURI
119144
* @param context
145+
* @param isolationLevel explicit isolation level, or null to prompt for default
120146
* @param tokens
121147
*/
122-
private void load(Repository repository, String baseURI, String context, final String... tokens) {
148+
private void load(Repository repository, String baseURI, String context, IsolationLevel isolationLevel,
149+
final String... tokens) {
123150
final String dataPath = tokens[1];
124151
URL dataURL = null;
125152
File dataFile = null;
@@ -136,7 +163,17 @@ private void load(Repository repository, String baseURI, String context, final S
136163
}
137164

138165
try {
139-
addData(repository, baseURI, context, dataURL, dataFile);
166+
IsolationLevel levelToUse = isolationLevel;
167+
if (levelToUse == null) {
168+
boolean confirmed = consoleIO
169+
.askProceed("No isolation level specified. Use isolation level NONE?", false);
170+
if (!confirmed) {
171+
return;
172+
}
173+
levelToUse = IsolationLevels.NONE;
174+
}
175+
176+
addData(repository, baseURI, context, dataURL, dataFile, levelToUse);
140177
} catch (RepositoryReadOnlyException e) {
141178
handleReadOnlyException(repository, e, tokens);
142179
} catch (MalformedURLException e) {
@@ -179,26 +216,37 @@ private void handleReadOnlyException(Repository repository, RepositoryReadOnlyEx
179216
/**
180217
* Add data from a URL or local file. If the dataURL is null, then the datafile will be used.
181218
*
182-
* @param repository repository
183-
* @param baseURI base URI
184-
* @param context context (can be null)
185-
* @param dataURL url of the data
186-
* @param dataFile file containing data
219+
* @param repository repository
220+
* @param baseURI base URI
221+
* @param context context (can be null)
222+
* @param dataURL url of the data
223+
* @param dataFile file containing data
224+
* @param isolationLevel isolation level to use for the transaction
187225
* @throws RepositoryException
188226
* @throws IOException
189227
* @throws RDFParseException
190228
*/
191-
private void addData(Repository repository, String baseURI, String context, URL dataURL, File dataFile)
192-
throws RepositoryException, IOException, RDFParseException {
229+
private void addData(Repository repository, String baseURI, String context, URL dataURL, File dataFile,
230+
IsolationLevel isolationLevel) throws RepositoryException, IOException, RDFParseException {
193231
Resource[] contexts = getContexts(repository, context);
194232
writeln("Loading data...");
195233

196234
final long startTime = System.nanoTime();
197235
try (RepositoryConnection con = repository.getConnection()) {
198-
if (dataURL == null) {
199-
con.add(dataFile, baseURI, null, contexts);
200-
} else {
201-
con.add(dataURL, baseURI, null, contexts);
236+
con.begin(isolationLevel);
237+
try {
238+
if (dataURL == null) {
239+
con.add(dataFile, baseURI, null, contexts);
240+
} else {
241+
con.add(dataURL, baseURI, null, contexts);
242+
}
243+
con.commit();
244+
} catch (RepositoryException | RDFParseException | IOException e) {
245+
con.rollback();
246+
throw e;
247+
} catch (RuntimeException e) {
248+
con.rollback();
249+
throw e;
202250
}
203251
}
204252
final long endTime = System.nanoTime();
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2024 Eclipse RDF4J contributors.
3+
*
4+
* All rights reserved. This program and the accompanying materials
5+
* are made available under the terms of the Eclipse Distribution License v1.0
6+
* which accompanies this distribution, and is available at
7+
* http://www.eclipse.org/org/documents/edl-v10.php.
8+
*
9+
* SPDX-License-Identifier: BSD-3-Clause
10+
*******************************************************************************/
11+
package org.eclipse.rdf4j.console.command;
12+
13+
import static org.mockito.ArgumentMatchers.any;
14+
import static org.mockito.ArgumentMatchers.anyString;
15+
import static org.mockito.ArgumentMatchers.contains;
16+
import static org.mockito.ArgumentMatchers.eq;
17+
import static org.mockito.ArgumentMatchers.isNull;
18+
import static org.mockito.Mockito.doNothing;
19+
import static org.mockito.Mockito.never;
20+
import static org.mockito.Mockito.verify;
21+
import static org.mockito.Mockito.when;
22+
23+
import java.io.File;
24+
25+
import org.eclipse.rdf4j.common.transaction.IsolationLevels;
26+
import org.eclipse.rdf4j.repository.Repository;
27+
import org.eclipse.rdf4j.repository.RepositoryConnection;
28+
import org.junit.jupiter.api.BeforeEach;
29+
import org.junit.jupiter.api.Test;
30+
import org.mockito.Mock;
31+
32+
public class LoadIsolationTest extends AbstractCommandTest {
33+
34+
@Mock
35+
private Repository mockRepository;
36+
37+
@Mock
38+
private RepositoryConnection mockConnection;
39+
40+
private Load cmd;
41+
42+
@BeforeEach
43+
public void setUp() throws Exception {
44+
cmd = new Load(mockConsoleIO, mockConsoleState, defaultSettings);
45+
46+
when(mockConsoleState.getRepository()).thenReturn(mockRepository);
47+
when(mockRepository.getConnection()).thenReturn(mockConnection);
48+
doNothing().when(mockConnection).add(any(File.class), isNull(), isNull(), any());
49+
}
50+
51+
@Test
52+
public void promptBeforeUsingDefaultIsolation() throws Exception {
53+
when(mockConsoleIO.askProceed(contains("isolation level NONE"), eq(false))).thenReturn(false);
54+
55+
cmd.execute("load", "data.ttl");
56+
57+
verify(mockConsoleIO).askProceed(contains("isolation level NONE"), eq(false));
58+
verify(mockConnection, never()).add(any(File.class), isNull(), isNull(), any());
59+
}
60+
61+
@Test
62+
public void allowsIsolationArgumentWithoutPrompt() throws Exception {
63+
cmd.execute("load", "data.ttl", "isolation", IsolationLevels.SNAPSHOT.name());
64+
65+
verify(mockConsoleIO, never()).askProceed(anyString(), eq(false));
66+
verify(mockConnection).begin(IsolationLevels.SNAPSHOT);
67+
}
68+
}

0 commit comments

Comments
 (0)