-
Notifications
You must be signed in to change notification settings - Fork 369
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: [Spanner] Add query options support #4512
Conversation
It's a wrapper around Google.Cloud.Spanner.Data.V1.ExecuteSqlRequest.Types.QueryOptions to avoid exposing protos in the API.
Verifying whether the implementation respects the precedence rules.
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.
Just a single comment for now so we can talk about the overall "effective options" approach. If it's not obvious what I'm talking about, I could potentially clone your fork and create a PR to show you what I mean :)
apis/Google.Cloud.Spanner.Data/Google.Cloud.Spanner.Data/SpannerCommand.cs
Outdated
Show resolved
Hide resolved
Also no longer setting QueryOptions when creating SpannerCommands.
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.
PTAL
apis/Google.Cloud.Spanner.Data/Google.Cloud.Spanner.Data.Tests/SpannerCommandTests.cs
Outdated
Show resolved
Hide resolved
apis/Google.Cloud.Spanner.Data/Google.Cloud.Spanner.Data/SpannerCommand.cs
Outdated
Show resolved
Hide resolved
Thanks @skuruppu - I'm at a conference today, but will look at this first thing Monday (assuming I don't get time to look today). @amanda-tarafa can also look in the meantime, of course :) |
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 - the GetEffectiveQueryOptions() approach is exactly what I had in mind, thanks :)
return null; | ||
} | ||
|
||
var queryOptionsProto = new V1.ExecuteSqlRequest.Types.QueryOptions(); |
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 can be simplified to use an object initializer:
return new V1.ExecuteSqlRequest.Types.QueryOptions { OptimizerVersion = optimizerVersion };
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.
I actually simplified the whole function and made it a bit more future-proof by using the proto merge functionality. That way when we add new options, the merging will happen automatically. The only thing to update manually would be reading the new env vars.
apis/Google.Cloud.Spanner.Data/Google.Cloud.Spanner.Data/SpannerCommand.ExecutableCommand.cs
Outdated
Show resolved
Hide resolved
apis/Google.Cloud.Spanner.Data/Google.Cloud.Spanner.Data/SpannerCommand.cs
Outdated
Show resolved
Hide resolved
apis/Google.Cloud.Spanner.Data/Google.Cloud.Spanner.Data/SpannerConnection.cs
Outdated
Show resolved
Hide resolved
@@ -84,11 +84,6 @@ public QueryOptions() : this(new V1.ExecuteSqlRequest.Types.QueryOptions()) | |||
return new QueryOptions(proto.Clone()); | |||
} | |||
|
|||
/// <summary> |
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.
If you don't really need the proto form, it might be worth just having the OptimizerVersion string property. You can still have a method to construct an instance from a proto if you want.
I don't much mind.
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 actually reverted this change because @larkee made me realize that merging protos is much easier than explicitly checking for each field. So leaving the protos for now but let me know if you think there's a better way to do this.
Use proto merging to simplify the logic and future-proof the code so that new options will be merged automatically.
// Returning null since we can't return a SpannerClient.ExecuteStreamingSqlStream() | ||
// since it's an abstract 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.
I couldn't find a type to return an empty result here so had to resort to null. It's not too bad because I don't care about the return value but I'm not sure if this is bad practice.
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 find the concrete class involved. Personally I generally avoid using mocks, preferring fakes - but I can see how a mock is possibly simpler here.
The lambda expression can be just (request, _) => null
though.
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 did find the concrete class but it's private to the implementation. But it's possible that I wasn't accessing it properly.
I'm happy to not return anything here so I simplified the lambda expr as you suggested.
I'm a bit uncertain about adding an integration test to this given that the backend won't respond in a noticeable way if a valid version was given. But if an invalid version was given it will let us know. @jskeet let me know your thoughts on whether this type of IT is typically added in this repo. |
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 it would be "good but not necessary" to have an integration test of "invalid optimizer version causes a failure". If you're happy to write the test (which shouldn't take long) I think it would be worth including.
Is there anything in the returned result that indicates the query optimizer version used, so we can test valid values?
// Returning null since we can't return a SpannerClient.ExecuteStreamingSqlStream() | ||
// since it's an abstract 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.
You could find the concrete class involved. Personally I generally avoid using mocks, preferring fakes - but I can see how a mock is possibly simpler here.
The lambda expression can be just (request, _) => null
though.
/// <summary> | ||
/// The query optimizer version configured in the options. | ||
/// </summary> | ||
public string OptimizerVersion |
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: simplify to
public string OptimizerVersion => Proto.OptimizerVersion;
public V1.ExecuteSqlRequest.Types.QueryOptions ToProto() => Proto.Clone(); | ||
|
||
/// <inheritdoc /> | ||
public bool Equals(QueryOptions other) |
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'd probably write this as:
public bool Equals(QueryOptions other) => other is object && OptimizerVersion == other.OptimizerVersion;
(As other.OptimizerVersion can never return null, and OptimizerVersion can never be null, you could just return OptimizerVersion == other?.OptimizerVersion
, but the reasoning required makes that harder to understand.)
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's neat but I agree that's harder to reason about. But I made the first change you suggested.
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.
PTAL
Now includes a couple of simple integration tests and they also pass :)
// Returning null since we can't return a SpannerClient.ExecuteStreamingSqlStream() | ||
// since it's an abstract 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.
I did find the concrete class but it's private to the implementation. But it's possible that I wasn't accessing it properly.
I'm happy to not return anything here so I simplified the lambda expr as you suggested.
public V1.ExecuteSqlRequest.Types.QueryOptions ToProto() => Proto.Clone(); | ||
|
||
/// <inheritdoc /> | ||
public bool Equals(QueryOptions other) |
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's neat but I agree that's harder to reason about. But I made the first change you suggested.
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.
Generally happy, with a few possible changes.
I might need to have a closer look at SpannerCommand and SpannerConnection to see what other properties do in terms of cloning - I have a feeling we sometimes create copies of those (sometimes through SpannerConnectionStringBuilder) which might need to be updated to propagate this, but I may be wrong. I'll have a look at the code separately - I think what's here would be good enough for a beta release though.
public QueryOptionsTests(ReadTableFixture fixture) => | ||
_fixture = fixture; | ||
|
||
private async Task<T> ExecuteAsync<T>(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.
I don't think this is actually used, is it?
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.
Yikes sorry, copy/pasta error.
} | ||
} | ||
|
||
// [START spanner_test_single_key_read_with_query_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.
It probably wouldn't hurt to add equivalent command-level option tests; I'll leave that as a call for you to make though.
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 added a success case but I think it would be redundant to have a failure case here since that's already tested.
} | ||
|
||
// Set the environment back. | ||
Environment.SetEnvironmentVariable(optimizerVersionVariable, savedOptimizerVersion); |
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.
It would be good to do this in a finally
block. You might even want to write a method accepting an Action
that will set the environment variable, execute the action, then reset the environment variable.
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.
Cool, didn't know about Action
. Updated to how I think actions should be used. Also using a finally
block now.
// variable. | ||
Mock<SpannerClient> spannerClientMock = SetupExecuteStreamingSql(cmdOptimizerVersion); | ||
|
||
const string optimizerVersion = "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.
Nit: rename to connOptimizerVersion
to clarify? The other two variables are pleasantly clear :)
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 :)
apis/Google.Cloud.Spanner.Data/Google.Cloud.Spanner.Data/QueryOptions.cs
Show resolved
Hide resolved
|
||
private QueryOptions(V1.ExecuteSqlRequest.Types.QueryOptions proto) => Proto = proto; | ||
|
||
/// <summary> |
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.
An optional alternative to this would be to have a static Empty
property, e.g.
public static QueryOptions Empty { get; } = new QueryOptions(new V1.ExecuteSqlRequest.Types.QueryOptions());
Then code which is currently new QueryOptions().WithOptimizerVersion(...)
would become QueryOptions.Empty.WithOptimizerVersion(...)
which might be clearer/simpler.
+amanda-tarafa do you have a preference on this 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.
I like this idea because I find it to be more readable so I changed it.
@@ -110,6 +110,12 @@ public override ConnectionState State | |||
|
|||
internal bool IsOpen => (State & ConnectionState.Open) == ConnectionState.Open; | |||
|
|||
/// <summary> |
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.
Currently this is only available as a property in the connection. Do you think we want people to be able to provide it in the connection string itself? I don't know what the expected use is likely to be.
We could potentially do that later.
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.
At the moment, I'm not sure. In contexts like the JDBC driver, it is specified in the connection string.
I would be happy to do this in a separate PR.
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.
Now that I've had another look:
- I believe we should be copying the property in SpannerCommand.Clone (which should be a matter of adding
QueryOptions = QueryOptions
within the call). - We can probably get away without it being in SpannerConnectionStringBuilder for now, but we should consider it for the future.
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 the review @js. All done. I didn't make the change for SpannerConnectionStringBuilder
in this PR but will be happy to try out what it looks like in a separate PR.
public QueryOptionsTests(ReadTableFixture fixture) => | ||
_fixture = fixture; | ||
|
||
private async Task<T> ExecuteAsync<T>(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.
Yikes sorry, copy/pasta error.
} | ||
} | ||
|
||
// [START spanner_test_single_key_read_with_query_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.
I added a success case but I think it would be redundant to have a failure case here since that's already tested.
} | ||
|
||
// Set the environment back. | ||
Environment.SetEnvironmentVariable(optimizerVersionVariable, savedOptimizerVersion); |
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.
Cool, didn't know about Action
. Updated to how I think actions should be used. Also using a finally
block now.
// variable. | ||
Mock<SpannerClient> spannerClientMock = SetupExecuteStreamingSql(cmdOptimizerVersion); | ||
|
||
const string optimizerVersion = "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.
Done :)
|
||
private QueryOptions(V1.ExecuteSqlRequest.Types.QueryOptions proto) => Proto = proto; | ||
|
||
/// <summary> |
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 like this idea because I find it to be more readable so I changed it.
@@ -110,6 +110,12 @@ public override ConnectionState State | |||
|
|||
internal bool IsOpen => (State & ConnectionState.Open) == ConnectionState.Open; | |||
|
|||
/// <summary> |
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.
At the moment, I'm not sure. In contexts like the JDBC driver, it is specified in the connection string.
I would be happy to do this in a separate PR.
Great, thanks for all of that. Are you now happy for me to squash into a single commit and merge? |
All good, sorry for the late reply. I will squash and merge now. |
@jskeet I have to ask you about cutting a release with these changes. What process should I follow? |
@skuruppu: Will email you internally. |
Adds the ability to set
QueryOptions
when running Cloud Spanner queries.For now, only setting the
query_optimizer_version
is added.QueryOptions
can be configured through the following mechanisms.SpannerConnection
level.SPANNER_OPTIMIZER_VERSION
environment variable.If the options are configured through multiple mechanisms then:
configured at the
SpannerConnection
level.SpannerConnection
or environment variable level.If no options are set, the optimizer version will default to:
backend will use the "latest" version.