Skip to content
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

feat: Add support for Explain feature #1852

Merged
merged 51 commits into from
May 19, 2022

Conversation

gauravsnj
Copy link
Contributor

@gauravsnj gauravsnj commented Apr 25, 2022

Adds the support for the EXPLAIN, which will ultimately be utilised by PG Adapter

@gauravsnj gauravsnj requested review from a team as code owners April 25, 2022 06:18
@product-auto-label product-auto-label bot added size: l Pull request size is large. api: spanner Issues related to the googleapis/java-spanner API. labels Apr 25, 2022
public ResultSet statementExplain(String sql){

sql = sql.trim();
String firstString = sql.split(" ")[0].toLowerCase();
Copy link
Collaborator

Choose a reason for hiding this comment

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

There are a couple of improvements that can be added to sql.split(..) here:

  1. In this specific case, we only need the first word, so we should add a limit to the split invocation to prevent the entire string from being analyzed. Also; as we need the second keyword further below, it would be better to call split only once to get the two first keywords instead of calling split twice.
  2. The keywords can be separated by any type of whitespace characters (so space, tab, linefeed, ...), and there could be more than one whitespace between keywords, so you should use split("\\s+")

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for suggesting these improvements. I've added them in the code

}

if(firstString.equals("analyze") || firstString.equals("analyse")) {
sql = sql.split(" ",2)[1];
Copy link
Collaborator

Choose a reason for hiding this comment

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

Here also use "\\s+" to split the string, and consider combining the two calls to split into one.

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

@@ -237,4 +251,290 @@ public void testStatementSetPgTransactionModeNoOp() {
verify(connection, never()).setTransactionMode(TransactionMode.READ_ONLY_TRANSACTION);
verify(connection, never()).setTransactionMode(TransactionMode.READ_WRITE_TRANSACTION);
}

private ResultSet getMockResultSetForAnalyseQuery(){
Copy link
Collaborator

Choose a reason for hiding this comment

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

Use Mockito to create a mock for this instead. See here for an example how to do that:

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 testStatementExplain(){
String sql = "verbose select * from table";
try {
ResultSet rs = subject.statementExplain(sql);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Use Assert.assertThrows to verify that a method invocation throws a specific exception. See here for an example:

assertThrows(IllegalArgumentException.class, () -> Dialect.fromName("INVALID"));

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

@@ -133,6 +133,14 @@
"method": "statementShowTransactionIsolationLevel",
"exampleStatements": ["show transaction isolation level","show variable transaction isolation level"]
},
{
"name": "EXPLAIN '<sql>'",
"executorName": "ClientSideStatementNoParamExecutor",
Copy link
Collaborator

Choose a reason for hiding this comment

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

This won't work, as the method that you have defined has a parameter; the sql string that is to be explained is the input parameter in this case. You have two options for this:

  1. You could reuse the ClientSideStatementSetExecutor for this. That executor is normally used for statements like SET <variable> = <value> where the value is the input parameter. In this case, the input parameter would be the entire sql string that follows EXPLAIN.
  2. You could create a custom executor specifically for this method. See ClientSideStatementPgBeginExecutor for an example for that.

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 think creating a new executor class will be better as ClientSideStatementSetExecutor is for specific type of commands and EXPLAIN clearly doesn't fit in that type. What do you say?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, that would be the neatest solution for this.

@product-auto-label product-auto-label bot added size: m Pull request size is medium. and removed size: l Pull request size is large. labels Apr 25, 2022
Copy link
Collaborator

@olavloite olavloite left a comment

Choose a reason for hiding this comment

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

This is looking good! Please add more tests for corner cases like explain foo, explain, explain\nselect * from foo etc.


@Override
public String convert(String value) {
return value.split(" +", 2)[1].toLowerCase();
Copy link
Collaborator

Choose a reason for hiding this comment

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

A couple of points regarding this:

  1. I think it should be programmed a little more defensively, so check that the array actually contains at least 2 items before trying to get the element at index 1, so we know that we won't run into any ArrayIndexOutOfBounds exceptions.
  2. It should probably be value.split("\\s+"). That ensures that the statement is also correctly handled if there is a line feed or a tab between EXPLAIN and the rest of the statement. Please also add tests for that.
  3. Why the .toLowerCase()? I don't think we need (or should) call that on the entire sql string. If we need it to determine whether the first token in the SQL string is one of the explain options, then we should only call toLowerCase() for that part. Otherwise, we will be sending a modified SQL string to the backend.

Comment on lines 459 to 461

sql = sql.trim();

Copy link
Collaborator

Choose a reason for hiding this comment

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

Remove

String[] arr = sql.split(" +", 2);

String option = arr[0];
String statementToBeExplained = arr[1];
Copy link
Collaborator

Choose a reason for hiding this comment

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

Check to ensure that we did not just receive something like explain foo, which would cause an ArrayOutOfBoundsException here. Also add tests for that kind of corner cases.

gauravsnj and others added 5 commits April 26, 2022 07:40
…nnection/ConnectionStatementExecutorImpl.java

Co-authored-by: Knut Olav Løite <koloite@gmail.com>
…nnection/ConnectionStatementExecutorImpl.java

Co-authored-by: Knut Olav Løite <koloite@gmail.com>
@product-auto-label product-auto-label bot added size: l Pull request size is large. and removed size: m Pull request size is medium. labels Apr 27, 2022
Copy link
Contributor

@ansh0l ansh0l left a comment

Choose a reason for hiding this comment

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

Have left a few comments, please take a look

@@ -442,4 +448,26 @@ public StatementResult statementShowRPCPriority() {
public StatementResult statementShowTransactionIsolationLevel() {
return resultSet("transaction_isolation", "serializable", SHOW_TRANSACTION_ISOLATION_LEVEL);
}

@Override
public StatementResult statementExplain(String sql) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Please add a comment on the intended support for Explain. Especially since there seem to be multiple permutations with EXPLAIN_OPTIONS.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

added

private final ExplainCommandConverter converter;
public static final Set<String> EXPLAIN_OPTIONS =
ImmutableSet.of(
"verbose", "costs", "settings", "buffers", "wal", "timing", "summary", "format");
Copy link
Contributor

Choose a reason for hiding this comment

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

From https://www.postgresql.org/docs/current/sql-explain.html, it looks like these keywords could be in uppercase. Are you doing any parsing etc to convert them to lowercase in input query?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes the parsing is being done before checking. Please check 548 and 512

return res;
}
throw SpannerExceptionFactory.newSpannerException(
ErrorCode.INVALID_ARGUMENT, String.format("Unknown transaction mode: %s", value));
Copy link
Contributor

Choose a reason for hiding this comment

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

Since this is the Explain method, is Unknown transaction mode in ErrorCode the relevant message?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed the error message

private final ExplainCommandConverter converter;
public static final Set<String> EXPLAIN_OPTIONS =
ImmutableSet.of(
"verbose", "costs", "settings", "buffers", "wal", "timing", "summary", "format");
Copy link
Contributor

Choose a reason for hiding this comment

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

From https://www.postgresql.org/docs/current/sql-explain.html, it looks like EXPLAIN (FORMAT JSON) SELECT * FROM foo; is a possible query, but I don't think its getting covered in your implementation.

Please check

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for pointing this out. I've added the code for handling this


ExplainCommandConverter converter = new ExplainCommandConverter();

/*Test for proper EXPLAIN*/
Copy link
Contributor

Choose a reason for hiding this comment

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

What does proper EXPLAIN mean? Are you referring for a valid EXPLAIN statement?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes it means valid explain.

@@ -0,0 +1,58 @@
/*
* Copyright 2019 Google LLC
Copy link
Contributor

Choose a reason for hiding this comment

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

2022 since this is a new file.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed

@product-auto-label product-auto-label bot added the size: xl Pull request size is extra large. label May 12, 2022

@RunWith(Parameterized.class)
public class ExplainCommandConverterTest {
@Parameter public Dialect dialect;
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we can remove this parameter and make this an unparameterized test. We only support explain for PostgreSQL databases.

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

@Test
public void testConvert() throws CompileException {
ExplainCommandConverter explainCommandConverter = new ExplainCommandConverter();
Assert.assertEquals(
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: add a static import for Assert.assertEquals to make the test case a little more compact

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

@@ -95,7 +95,7 @@ long executeStreamingPartitionedUpdate(
resumeToken = rs.getResumeToken();
}
if (rs.hasStats()) {
foundStats = true;
foundStats = rs.getStats().hasRowCountLowerBound();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this change needed? Or did this not work in the current implementation?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

DatabaseClientImplTest.testExecutePartitionedDmlWithQuery test was failing without this line. I took this change from your PR (feat: support analyzeUpdate #1867 )

String arr2[] = probableOptions.split("\\s+");
if(arr2.length >= 3){
isAnalyse = false;
break;
Copy link
Collaborator

Choose a reason for hiding this comment

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

That sounds reasonable to me.

}

if (sql.charAt(0) == '(') {
int index = sql.indexOf(')');
Copy link
Collaborator

Choose a reason for hiding this comment

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

This needs a quick check to verify that there actually is a closing parenthesis. If there is not, we should return a reasonable error message (something like 'missing closing parenthesis'), or otherwise just send the entire SQL string to the backend and let that determine what the error should be. Now it will probably fail with something like an IndexOutOfBoundsException, as index is -1.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

added


@Test
public void testValidExplainWithFalseAnalyse() {
String statement = " explain (analyse false) " + EXPLAIN_STATEMENT_QUERY;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Here and elsewhere: PostgreSQL uses analyze instead of analyse in all documentation. I know both work, but I think it would be better if we also stuck to analyze (except for tests that verify that analyse also works).

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

@gauravsnj gauravsnj added the kokoro:force-run Add this label to force Kokoro to re-run the tests. label May 13, 2022
@yoshi-kokoro yoshi-kokoro removed the kokoro:force-run Add this label to force Kokoro to re-run the tests. label May 13, 2022
@gauravsnj gauravsnj added kokoro:force-run Add this label to force Kokoro to re-run the tests. owlbot:run Add this label to trigger the Owlbot post processor. labels May 13, 2022
@gcf-owl-bot gcf-owl-bot bot removed the owlbot:run Add this label to trigger the Owlbot post processor. label May 13, 2022
@yoshi-kokoro yoshi-kokoro removed the kokoro:force-run Add this label to force Kokoro to re-run the tests. label May 13, 2022
.bind("table_name")
.to(table.toUpperCase())
String.format(
"SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE UPPER(TABLE_NAME)=UPPER(\'%s\')",
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: We should never use string concatenation to add literals to a query, as it could be vulnerable to SQL injection attacks. It's not really relevant here, but it's good practice to always use statement parameters.

@olavloite olavloite requested a review from ansh0l May 17, 2022 10:30
@olavloite olavloite added the owlbot:run Add this label to trigger the Owlbot post processor. label May 17, 2022
@gcf-owl-bot gcf-owl-bot bot removed the owlbot:run Add this label to trigger the Owlbot post processor. label May 17, 2022
Copy link
Contributor

@ansh0l ansh0l left a comment

Choose a reason for hiding this comment

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

LGTM with minor nits.

String option = arr[0].toLowerCase();
String statementToBeExplained = arr[1];

if (ClientSideStatementExplainExecutor.EXPLAIN_OPTIONS.contains(option)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

ack

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api: spanner Issues related to the googleapis/java-spanner API. size: xl Pull request size is extra large.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants