-
Notifications
You must be signed in to change notification settings - Fork 131
feat: Add support for Explain feature #1852
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
Conversation
public ResultSet statementExplain(String sql){ | ||
|
||
sql = sql.trim(); | ||
String firstString = sql.split(" ")[0].toLowerCase(); |
There was a problem hiding this comment.
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:
- 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 callsplit
only once to get the two first keywords instead of callingsplit
twice. - 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+")
There was a problem hiding this comment.
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]; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
...-spanner/src/main/resources/com/google/cloud/spanner/connection/PG_ClientSideStatements.json
Outdated
Show resolved
Hide resolved
@@ -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(){ |
There was a problem hiding this comment.
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:
Line 196 in 12a0acb
ResultSet mockResultSet = mock(ResultSet.class); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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:
java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DialectTest.java
Line 93 in 5f156f2
assertThrows(IllegalArgumentException.class, () -> Dialect.fromName("INVALID")); |
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
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:
- You could reuse the
ClientSideStatementSetExecutor
for this. That executor is normally used for statements likeSET <variable> = <value>
where thevalue
is the input parameter. In this case, the input parameter would be the entire sql string that followsEXPLAIN
. - You could create a custom executor specifically for this method. See
ClientSideStatementPgBeginExecutor
for an example for that.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
…er/connection/PG_ClientSideStatements.json Co-authored-by: Knut Olav Løite <[email protected]>
There was a problem hiding this 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(); |
There was a problem hiding this comment.
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:
- 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. - 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 betweenEXPLAIN
and the rest of the statement. Please also add tests for that. - 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 theexplain
options, then we should only calltoLowerCase()
for that part. Otherwise, we will be sending a modified SQL string to the backend.
...anner/src/main/java/com/google/cloud/spanner/connection/ConnectionStatementExecutorImpl.java
Outdated
Show resolved
Hide resolved
...anner/src/main/java/com/google/cloud/spanner/connection/ConnectionStatementExecutorImpl.java
Outdated
Show resolved
Hide resolved
|
||
sql = sql.trim(); | ||
|
There was a problem hiding this comment.
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]; |
There was a problem hiding this comment.
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.
…nnection/ConnectionStatementExecutorImpl.java Co-authored-by: Knut Olav Løite <[email protected]>
…nnection/ConnectionStatementExecutorImpl.java Co-authored-by: Knut Olav Løite <[email protected]>
There was a problem hiding this 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) { |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return res; | ||
} | ||
throw SpannerExceptionFactory.newSpannerException( | ||
ErrorCode.INVALID_ARGUMENT, String.format("Unknown transaction mode: %s", value)); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed the error message
...er/src/main/java/com/google/cloud/spanner/connection/ClientSideStatementValueConverters.java
Outdated
Show resolved
Hide resolved
private final ExplainCommandConverter converter; | ||
public static final Set<String> EXPLAIN_OPTIONS = | ||
ImmutableSet.of( | ||
"verbose", "costs", "settings", "buffers", "wal", "timing", "summary", "format"); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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*/ |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed
|
||
@RunWith(Parameterized.class) | ||
public class ExplainCommandConverterTest { | ||
@Parameter public Dialect dialect; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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(')'); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added
google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ExplainTest.java
Outdated
Show resolved
Hide resolved
google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ExplainTest.java
Outdated
Show resolved
Hide resolved
google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ExplainTest.java
Outdated
Show resolved
Hide resolved
|
||
@Test | ||
public void testValidExplainWithFalseAnalyse() { | ||
String statement = " explain (analyse false) " + EXPLAIN_STATEMENT_QUERY; |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ExplainTest.java
Outdated
Show resolved
Hide resolved
.bind("table_name") | ||
.to(table.toUpperCase()) | ||
String.format( | ||
"SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE UPPER(TABLE_NAME)=UPPER(\'%s\')", |
There was a problem hiding this comment.
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.
There was a problem hiding this 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)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ack
Adds the support for the EXPLAIN, which will ultimately be utilised by PG Adapter