Skip to content

QL: Add leniency option to SQL CLI #83795

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Feb 16, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/changelog/83795.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pr: 83795
summary: Add leniency option to SQL CLI
area: SQL
type: enhancement
issues:
- 67436
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import java.io.PipedOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
Expand Down Expand Up @@ -65,6 +66,15 @@ public class EmbeddedCli implements Closeable {

public EmbeddedCli(String elasticsearchAddress, boolean checkConnectionOnStartup, @Nullable SecurityConfig security)
throws IOException {
this(elasticsearchAddress, checkConnectionOnStartup, security, new String[0]);
}

public EmbeddedCli(
String elasticsearchAddress,
boolean checkConnectionOnStartup,
@Nullable SecurityConfig security,
@Nullable String[] additionalArgs
) throws IOException {
PipedOutputStream outgoing = new PipedOutputStream();
PipedInputStream cliIn = new PipedInputStream(outgoing);
PipedInputStream incoming = new PipedInputStream();
Expand Down Expand Up @@ -109,6 +119,10 @@ protected boolean addShutdownHook() {
args.add(Boolean.toString(randomBoolean()));
}

if (additionalArgs != null) {
Arrays.stream(additionalArgs).forEach(x -> args.add(x));
}

exec = new Thread(() -> {
try {
/*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,45 @@ public void testSelectWithWhere() throws IOException {
assertThat(readLine(), RegexMatcher.matches("\\s*2\\s*\\|\\s*test_value2\\s*"));
assertEquals("", readLine());
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would create a test class just for this command. And, apart from the test you have below, I'd test the false scenario as well (the one that generates an error).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

public void testLeniency() throws IOException {
EmbeddedCli cliWithLeniency = new EmbeddedCli(
CliIntegrationTestCase.elasticsearchAddress(),
true,
securityConfig(),
new String[] { "-lenient", "true" }
);

try {
index("test", body -> body.field("name", "foo").field("tags", new String[] { "bar", "bar" }));

assertThat(cliWithLeniency.command("SELECT * FROM test"), RegexMatcher.matches("\\s*name\\s*\\|\\s*tags\\s*"));
assertThat(cliWithLeniency.readLine(), containsString("----------"));
assertThat(cliWithLeniency.readLine(), RegexMatcher.matches("\\s*foo\\s*\\|\\s*bar\\s*"));
assertEquals("", cliWithLeniency.readLine());
} finally {
cliWithLeniency.close();
}
}

public void testLeniency2() throws IOException {
EmbeddedCli cliWithLeniency = new EmbeddedCli(
CliIntegrationTestCase.elasticsearchAddress(),
true,
securityConfig(),
new String[] { "-l", "true" }
);

try {
index("test", body -> body.field("name", "foo").field("tags", new String[] { "bar", "bar" }));

assertThat(cliWithLeniency.command("SELECT * FROM test"), RegexMatcher.matches("\\s*name\\s*\\|\\s*tags\\s*"));
assertThat(cliWithLeniency.readLine(), containsString("----------"));
assertThat(cliWithLeniency.readLine(), RegexMatcher.matches("\\s*foo\\s*\\|\\s*bar\\s*"));
assertEquals("", cliWithLeniency.readLine());
} finally {
cliWithLeniency.close();
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public class Cli extends Command {
private final OptionSpec<Boolean> checkOption;
private final OptionSpec<String> connectionString;
private final OptionSpec<Boolean> binaryCommunication;
private final OptionSpec<Boolean> lenientOption;

/**
* Use this VM Options to run in IntelliJ or Eclipse:
Expand Down Expand Up @@ -102,6 +103,11 @@ public Cli(CliTerminal cliTerminal) {
.withRequiredArg()
.ofType(Boolean.class)
.defaultsTo(Boolean.parseBoolean(System.getProperty("cli.check", "true")));
this.lenientOption = parser.acceptsAll(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the already existent commands and options for CLI, I would have inclined to make lenient a command and not a cli option. For example, fetch_size is a command (FetchSizeCliCommand class). The command can be executed multiple times during the life of the cli and an user can change the leniency with each query executed.

Copy link
Contributor

@bpintea bpintea Feb 10, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the session configuration point of view (and maybe consistency?), this would make indeed sense.
From a practical POV, not sure if changing this behavior on a query-by-query case would be as useful, but for those users that use the CLI integrated into other scripts, having it as an argument might be preferred.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, having a command would make it much more flexible, I'm implementing it now.
Do you think it makes sense to keep both (ie. the command AND the cli option) or should we just have the command?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't make sense to have it in both places. We are not doing this for any setting/config so far. Please, implement it as a command only.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I removed the CLI option and replaced it with a command for now (lenient = true/false).
In case we decide to go with both, re-enabling the option is straight forward.

Arrays.asList("l", "lenient"),
"Lenient and return first value for fields with multiple values"
).withRequiredArg().ofType(Boolean.class).defaultsTo(Boolean.parseBoolean(System.getProperty("cli.lenient", "false")));

this.connectionString = parser.nonOptions("uri");
}

Expand All @@ -110,6 +116,7 @@ protected void execute(org.elasticsearch.cli.Terminal terminal, OptionSet option
boolean debug = options.has("d") || options.has("debug");
boolean binary = binaryCommunication.value(options);
boolean checkConnection = checkOption.value(options);
boolean lenient = this.lenientOption.value(options);
List<String> args = connectionString.values(options);
if (args.size() > 1) {
throw new UserException(ExitCodes.USAGE, "expecting a single uri");
Expand All @@ -120,17 +127,18 @@ protected void execute(org.elasticsearch.cli.Terminal terminal, OptionSet option
throw new UserException(ExitCodes.USAGE, "expecting a single keystore file");
}
String keystoreLocationValue = args.size() == 1 ? args.get(0) : null;
execute(uri, debug, binary, keystoreLocationValue, checkConnection);
execute(uri, debug, binary, keystoreLocationValue, checkConnection, lenient);
}

private void execute(String uri, boolean debug, boolean binary, String keystoreLocation, boolean checkConnection) throws Exception {
private void execute(String uri, boolean debug, boolean binary, String keystoreLocation, boolean checkConnection, boolean lenient)
throws Exception {
CliCommand cliCommand = new CliCommands(
new PrintLogoCommand(),
new ClearScreenCliCommand(),
new FetchSizeCliCommand(),
new FetchSeparatorCliCommand(),
new ServerInfoCliCommand(),
new ServerQueryCliCommand()
new ServerQueryCliCommand(lenient)
);
try {
ConnectionBuilder connectionBuilder = new ConnectionBuilder(cliTerminal);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.elasticsearch.xpack.sql.cli.CliTerminal;
import org.elasticsearch.xpack.sql.client.HttpClient;
import org.elasticsearch.xpack.sql.client.JreHttpUrlConnection;
import org.elasticsearch.xpack.sql.proto.CoreProtocol;
import org.elasticsearch.xpack.sql.proto.Mode;
import org.elasticsearch.xpack.sql.proto.SqlQueryResponse;
import org.elasticsearch.xpack.sql.proto.formatter.SimpleFormatter;
Expand All @@ -19,14 +20,24 @@

public class ServerQueryCliCommand extends AbstractServerCliCommand {

protected boolean lenient;

public ServerQueryCliCommand() {
this(CoreProtocol.FIELD_MULTI_VALUE_LENIENCY);
}

public ServerQueryCliCommand(boolean lenient) {
this.lenient = lenient;
}

@Override
protected boolean doHandle(CliTerminal terminal, CliSession cliSession, String line) {
SqlQueryResponse response = null;
HttpClient cliClient = cliSession.getClient();
SimpleFormatter formatter;
String data;
try {
response = cliClient.basicQuery(line, cliSession.getFetchSize());
response = cliClient.basicQuery(line, cliSession.getFetchSize(), lenient);
formatter = new SimpleFormatter(response.columns(), response.rows(), CLI);
data = formatter.formatWithHeader(response.columns(), response.rows());
while (true) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ public void testExceptionHandling() throws Exception {
TestTerminal testTerminal = new TestTerminal();
HttpClient client = mock(HttpClient.class);
CliSession cliSession = new CliSession(client);
when(client.basicQuery("blah", 1000)).thenThrow(new SQLException("test exception"));
when(client.basicQuery("blah", 1000, false)).thenThrow(new SQLException("test exception"));
ServerQueryCliCommand cliCommand = new ServerQueryCliCommand();
assertTrue(cliCommand.handle(testTerminal, cliSession, "blah"));
assertEquals("<b>Bad request [</b><i>test exception</i><b>]</b>\n", testTerminal.toString());
verify(client, times(1)).basicQuery(eq("blah"), eq(1000));
verify(client, times(1)).basicQuery(eq("blah"), eq(1000), eq(false));
verifyNoMoreInteractions(client);
}

Expand All @@ -45,15 +45,15 @@ public void testOnePageQuery() throws Exception {
HttpClient client = mock(HttpClient.class);
CliSession cliSession = new CliSession(client);
cliSession.setFetchSize(10);
when(client.basicQuery("test query", 10)).thenReturn(fakeResponse("", true, "foo"));
when(client.basicQuery("test query", 10, false)).thenReturn(fakeResponse("", true, "foo"));
ServerQueryCliCommand cliCommand = new ServerQueryCliCommand();
assertTrue(cliCommand.handle(testTerminal, cliSession, "test query"));
assertEquals("""
field \s
---------------
foo \s
<flush/>""", testTerminal.toString());
verify(client, times(1)).basicQuery(eq("test query"), eq(10));
verify(client, times(1)).basicQuery(eq("test query"), eq(10), eq(false));
verifyNoMoreInteractions(client);
}

Expand All @@ -62,7 +62,7 @@ public void testThreePageQuery() throws Exception {
HttpClient client = mock(HttpClient.class);
CliSession cliSession = new CliSession(client);
cliSession.setFetchSize(10);
when(client.basicQuery("test query", 10)).thenReturn(fakeResponse("my_cursor1", true, "first"));
when(client.basicQuery("test query", 10, false)).thenReturn(fakeResponse("my_cursor1", true, "first"));
when(client.nextPage("my_cursor1")).thenReturn(fakeResponse("my_cursor2", false, "second"));
when(client.nextPage("my_cursor2")).thenReturn(fakeResponse("", false, "third"));
ServerQueryCliCommand cliCommand = new ServerQueryCliCommand();
Expand All @@ -74,7 +74,7 @@ public void testThreePageQuery() throws Exception {
second \s
third \s
<flush/>""", testTerminal.toString());
verify(client, times(1)).basicQuery(eq("test query"), eq(10));
verify(client, times(1)).basicQuery(eq("test query"), eq(10), eq(false));
verify(client, times(2)).nextPage(any());
verifyNoMoreInteractions(client);
}
Expand All @@ -86,7 +86,7 @@ public void testTwoPageQueryWithSeparator() throws Exception {
cliSession.setFetchSize(15);
// Set a separator
cliSession.setFetchSeparator("-----");
when(client.basicQuery("test query", 15)).thenReturn(fakeResponse("my_cursor1", true, "first"));
when(client.basicQuery("test query", 15, false)).thenReturn(fakeResponse("my_cursor1", true, "first"));
when(client.nextPage("my_cursor1")).thenReturn(fakeResponse("", false, "second"));
ServerQueryCliCommand cliCommand = new ServerQueryCliCommand();
assertTrue(cliCommand.handle(testTerminal, cliSession, "test query"));
Expand All @@ -97,7 +97,7 @@ public void testTwoPageQueryWithSeparator() throws Exception {
-----
second \s
<flush/>""", testTerminal.toString());
verify(client, times(1)).basicQuery(eq("test query"), eq(15));
verify(client, times(1)).basicQuery(eq("test query"), eq(15), eq(false));
verify(client, times(1)).nextPage(any());
verifyNoMoreInteractions(client);
}
Expand All @@ -107,7 +107,7 @@ public void testCursorCleanupOnError() throws Exception {
HttpClient client = mock(HttpClient.class);
CliSession cliSession = new CliSession(client);
cliSession.setFetchSize(15);
when(client.basicQuery("test query", 15)).thenReturn(fakeResponse("my_cursor1", true, "first"));
when(client.basicQuery("test query", 15, false)).thenReturn(fakeResponse("my_cursor1", true, "first"));
when(client.nextPage("my_cursor1")).thenThrow(new SQLException("test exception"));
when(client.queryClose("my_cursor1", Mode.CLI)).thenReturn(true);
ServerQueryCliCommand cliCommand = new ServerQueryCliCommand();
Expand All @@ -118,7 +118,7 @@ public void testCursorCleanupOnError() throws Exception {
first \s
<b>Bad request [</b><i>test exception</i><b>]</b>
""", testTerminal.toString());
verify(client, times(1)).basicQuery(eq("test query"), eq(15));
verify(client, times(1)).basicQuery(eq("test query"), eq(15), eq(false));
verify(client, times(1)).nextPage(any());
verify(client, times(1)).queryClose(eq("my_cursor1"), eq(Mode.CLI));
verifyNoMoreInteractions(client);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ public MainResponse serverInfo() throws SQLException {
}

public SqlQueryResponse basicQuery(String query, int fetchSize) throws SQLException {
return basicQuery(query, fetchSize, CoreProtocol.FIELD_MULTI_VALUE_LENIENCY);
}

public SqlQueryResponse basicQuery(String query, int fetchSize, boolean fieldMultiValueLeniency) throws SQLException {
// TODO allow customizing the time zone - this is what session set/reset/get should be about
// method called only from CLI
SqlQueryRequest sqlRequest = new SqlQueryRequest(
Expand All @@ -74,7 +78,7 @@ public SqlQueryResponse basicQuery(String query, int fetchSize) throws SQLExcept
Boolean.FALSE,
null,
new RequestInfo(Mode.CLI, ClientVersion.CURRENT),
false,
fieldMultiValueLeniency,
false,
cfg.binaryCommunication()
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ private void assertBinaryRequestForCLI(XContentType xContentType) throws URISynt

prepareMockResponse();
try {
httpClient.basicQuery(query, fetchSize);
httpClient.basicQuery(query, fetchSize, false);
} catch (SQLException e) {
logger.info("Ignored SQLException", e);
}
Expand Down