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

Statement parameter binding #87

Merged
merged 15 commits into from
Jun 4, 2019
Merged

Statement parameter binding #87

merged 15 commits into from
Jun 4, 2019

Conversation

dmitry-s
Copy link
Contributor

fixes #13

…-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

# 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
@dmitry-s dmitry-s requested review from elefeint, dzou and meltsufin and removed request for dzou May 30, 2019 21:00
@ChengyuanZhao ChengyuanZhao changed the title Codecs refactoring Statement parameter binding May 31, 2019
Copy link
Contributor

@elefeint elefeint left a 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);
Copy link
Contributor

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.

Copy link
Member

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.

Copy link
Member

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")) {
Copy link
Contributor

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")) {
Copy link
Contributor

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()) {
Copy link
Contributor

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 Structs.

Copy link
Contributor

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,
Copy link
Contributor

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()?

Copy link
Member

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.

Copy link
Member

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<>();
Copy link
Contributor

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();
Copy link
Contributor

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) {
Copy link
Contributor

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);
Copy link
Member

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);
Copy link
Member

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");
Copy link
Member

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.

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 thought we decided that we don't want to support that, because it would require parsing of the SQL string.

Copy link
Member

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.

Copy link
Member

Choose a reason for hiding this comment

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

#91

}

@Override
public Statement bindNull(int i, Class<?> type) {
return null;
throw new IllegalArgumentException("Only named parameters are supported");
Copy link
Member

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,
Copy link
Member

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<>();
Copy link
Member

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 Structs in the bind immediately, rather than first keeping them as maps and then converting to structs here?

}

@Test
public void executeDummyImplementationBind() {
Copy link
Member

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(
Copy link
Member

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?

elefeint
elefeint previously approved these changes Jun 3, 2019
Copy link
Contributor

@elefeint elefeint left a 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");
Copy link
Contributor

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.

@elefeint
Copy link
Contributor

elefeint commented Jun 3, 2019

Integration test is unhappy tho:

SEVERE: *~*~*~ Channel ManagedChannelImpl{logId=41, target=spanner.googleapis.com:443} was not shutdown properly!!! ~*~*~*
    Make sure to call shutdown()/shutdownNow() and wait until awaitTermination() returns true.
java.lang.RuntimeException: ManagedChannel allocation site

}
return this;
}
throw new IllegalArgumentException("Only String identifiers are supported");
Copy link
Contributor

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(..)

Copy link
Member

@meltsufin meltsufin left a 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.

…-r2dbc into codecs-refactoring

# Conflicts:
#	src/test/java/com/google/cloud/spanner/r2dbc/SpannerStatementTest.java
ChengyuanZhao
ChengyuanZhao previously approved these changes Jun 4, 2019
elefeint
elefeint previously approved these changes Jun 4, 2019
Copy link
Contributor

@elefeint elefeint left a 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.

dzou
dzou previously approved these changes Jun 4, 2019
@dmitry-s dmitry-s dismissed stale reviews from dzou, elefeint, and ChengyuanZhao via d551ba9 June 4, 2019 20:15
dzou
dzou previously approved these changes Jun 4, 2019
@meltsufin meltsufin dismissed stale reviews from dzou via 32a429d June 4, 2019 20:26
@dmitry-s dmitry-s merged commit 43b1284 into master Jun 4, 2019
@dmitry-s dmitry-s deleted the codecs-refactoring branch June 4, 2019 21:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Implement parameter binding
5 participants