-
Notifications
You must be signed in to change notification settings - Fork 29
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
Statement parameter binding #87
Conversation
…-r2dbc into codecs-refactoring # Conflicts: # src/test/java/com/google/cloud/spanner/r2dbc/codecs/DefaultCodecsNegativeTest.java
…-r2dbc into codecs-refactoring # Conflicts: # src/test/java/com/google/cloud/spanner/r2dbc/codecs/DefaultCodecsNegativeTest.java
…-r2dbc into codecs-refactoring
…-r2dbc into codecs-refactoring # Conflicts: # src/test/java/com/google/cloud/spanner/r2dbc/codecs/DefaultCodecsNegativeTest.java
…-r2dbc into codecs-refactoring # Conflicts: # src/main/java/com/google/cloud/spanner/r2dbc/SpannerStatement.java # src/main/java/com/google/cloud/spanner/r2dbc/client/Client.java # src/main/java/com/google/cloud/spanner/r2dbc/client/GrpcClient.java
…-r2dbc into codecs-refactoring # Conflicts: # src/main/java/com/google/cloud/spanner/r2dbc/client/GrpcClient.java # src/test/java/com/google/cloud/spanner/r2dbc/it/SpannerIT.java
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.
Looks good; I would just rearrange a bit what information we are accumulating and how.
public Statement bind(Object identifier, Object value) { | ||
requireNonNull(identifier); | ||
if (identifier instanceof String) { | ||
this.bindings.getLast().put((String)identifier, 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.
LinkedList.getLast() is efficient, but if we keep a map of in-progress/incomplete bindings separately, then there is no need for getting it, and also no need to add an empty list in constructor.
Then add()
would add the incomplete map (that has just become complete) and get a new map going.
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 guess the only thing here would be that after completing the last one, you'd have to call add()
just to put it into the completed list, or deal with merging completed and incomplete somewhere else.
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.
Yea I think that might complicate things. Especially since the user doesn't need to call add() to close/finalize the final one. That would mean some sort of code in the execute() that would add on this final map, which feels out of place to be in in the execute() method.
|
||
Flux<Struct> structFlux = Flux.fromIterable(paramsStructs); | ||
|
||
if (this.sql != null && this.sql.trim().toLowerCase().startsWith("select")) { |
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.
Could you add assertion for sql
being non-null to the constructor? A statement with null sql would not be valid, so we should fail early on that.
|
||
Flux<Struct> structFlux = Flux.fromIterable(paramsStructs); | ||
|
||
if (this.sql != null && this.sql.trim().toLowerCase().startsWith("select")) { |
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 know it's trivial logic, but can the second half of the condition go into a isSelectQuery
helper method?
Builder paramsStructBuilder = Struct.newBuilder(); | ||
Map<String, Type> types = this.types.isEmpty() ? new HashMap<>() : null; | ||
|
||
for (Map.Entry<String, Object> binding : bindingsBatch.entrySet()) { |
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.
Would it make sense to put the logic in a helper method and call it from .add()
? We are building up a map, might as well be building up a list of Struct
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.
Yeah I would recommend this approach too. You could keep a List<Struct.Builder>
instead of Map which will save a layer of conversions; then just build them all when you are ready to execute the query.
|
||
for (Map.Entry<String, Object> binding : bindingsBatch.entrySet()) { | ||
String paramName = binding.getKey(); | ||
Codec codec = this.resolvedCodecs.computeIfAbsent(paramName, |
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.
The same binding for all rows should be of the same type, right? Can we save time by only doing this on the first add()
?
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.
How do we know that all rows will use the same type for a particular column? Is that the contract in R2DBC?
Frankly, I would try to avoid this resolvedCodecs
caching altogether. If anything, Codecs
itself can cache based on type instead. There doesn't seem to be an advantage to caching here vs in the Codecs
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.
These aren't actually columns if i'm not mistaken. These are param tags in the query string, which means they don't need to be the same type. For example , you can get the size in bytes in googleSQL of a STRING or BYTES, so that single tag could take on either value.
Furthermore, I think the codec can change too? For example, if the tag in the SQL is like where 100 < @tag
then could two different codecs convert either a long or a string into a number to fill that tag?
|
||
private String sql; | ||
|
||
private LinkedList<Map<String, Object>> bindings = new LinkedList<>(); |
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 in practice ArrayList is typically preferred over LinkedList because of caching advantages.
} | ||
|
||
@Override | ||
public Publisher<? extends Result> execute() { | ||
List<Struct> paramsStructs = new ArrayList<>(); | ||
for (Map<String, Object> bindingsBatch : this.bindings) { | ||
Builder paramsStructBuilder = Struct.newBuilder(); |
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.
minor style thing - it would be better to qualify this Builder
class with Struct.Builder
|
||
if (transaction != null) { | ||
if (transactionContext != null && transactionContext.getTransaction() != null) { |
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 you should just add a Assert.notNull(transaction)
in the constructor of TransactionContext, and then simplify this to just check transactionContext != null
. This way users in the future will not have to worry about doing 2 null-checks for transactions.
@@ -100,7 +100,8 @@ public Batch createBatch() { | |||
|
|||
@Override | |||
public Statement createStatement(String sql) { | |||
return new SpannerStatement(this.client, this.session, this.currentTransaction, sql); | |||
return new SpannerStatement(this.client, this.session, | |||
SpannerTransactionContext.from(this.currentTransaction), 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.
Why do SpannerTransactionContext.from
here rather than in the SpannerStatement
?
Or, why not replace the currentTransaction
in SpannerConnection
with SpannerTransactionContext
?
public Statement bind(Object identifier, Object value) { | ||
requireNonNull(identifier); | ||
if (identifier instanceof String) { | ||
this.bindings.getLast().put((String)identifier, 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.
I guess the only thing here would be that after completing the last one, you'd have to call add()
just to put it into the completed list, or deal with merging completed and incomplete somewhere else.
} | ||
|
||
@Override | ||
public Statement bind(int i, Object o) { | ||
return null; | ||
throw new IllegalArgumentException("Only named parameters are supported"); |
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.
Do we have an issue to track support for index based binding?
I think UnsupportedOperationException
is more appropriate here.
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 thought we decided that we don't want to support that, because it would require parsing of the SQL string.
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.
Either way, let's track this as an issue.
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.
} | ||
|
||
@Override | ||
public Statement bindNull(int i, Class<?> type) { | ||
return null; | ||
throw new IllegalArgumentException("Only named parameters are supported"); |
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.
Ditto UnsupportedOperationException
.
|
||
for (Map.Entry<String, Object> binding : bindingsBatch.entrySet()) { | ||
String paramName = binding.getKey(); | ||
Codec codec = this.resolvedCodecs.computeIfAbsent(paramName, |
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.
How do we know that all rows will use the same type for a particular column? Is that the contract in R2DBC?
Frankly, I would try to avoid this resolvedCodecs
caching altogether. If anything, Codecs
itself can cache based on type instead. There doesn't seem to be an advantage to caching here vs in the Codecs
class.
} | ||
|
||
@Override | ||
public Publisher<? extends Result> execute() { | ||
List<Struct> paramsStructs = new ArrayList<>(); |
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 there anything wrong with building the Struct
s in the bind immediately, rather than first keeping them as maps and then converting to structs here?
} | ||
|
||
@Test | ||
public void executeDummyImplementationBind() { |
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.
Can you add some comments in this method to separate blocks that are setting up the dummy values and mocks vs. where the actual method under test is executed? These reactive tests are a pain to read. Hopefully, some comments would help.
|
||
DatabaseAdminClient dbAdminClient = spanner.getDatabaseAdminClient(); | ||
|
||
dbAdminClient.updateDatabaseDdl( |
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 not exactly setting up the stubs. :)
Rename the method and/or put this in a separate method?
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.
Excellent, this unblocks the remainder of integration testing.
return structFlux.concatMapDelayError(this::runSingleStatement); | ||
} | ||
|
||
private boolean isSelectQuery() { | ||
return this.sql.trim().toLowerCase().startsWith("select"); |
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.
optimization for the future: only lowercasing the prefix. Does not need to be in this PR.
Integration test is unhappy tho:
|
} | ||
return this; | ||
} | ||
throw new IllegalArgumentException("Only String identifiers are supported"); |
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.
You could make this into a helper in the Assert
helper; i.e. Assert.requireInstanceOf(..)
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 update to use StepVerifier
in tests.
src/main/java/com/google/cloud/spanner/r2dbc/SpannerStatement.java
Outdated
Show resolved
Hide resolved
src/main/java/com/google/cloud/spanner/r2dbc/SpannerStatement.java
Outdated
Show resolved
Hide resolved
src/main/java/com/google/cloud/spanner/r2dbc/SpannerStatement.java
Outdated
Show resolved
Hide resolved
…-r2dbc into codecs-refactoring # Conflicts: # src/test/java/com/google/cloud/spanner/r2dbc/SpannerStatementTest.java
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.
Let's get it in, and deal with the integration test separately.
d551ba9
fixes #13