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: [Spanner] Add query options support #4512

Merged
merged 29 commits into from
Mar 14, 2020

Conversation

skuruppu
Copy link
Contributor

@skuruppu skuruppu commented Mar 5, 2020

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.

  1. At the SpannerConnection level.
  2. Through the SPANNER_OPTIMIZER_VERSION environment variable.
  3. At a query level.

If the options are configured through multiple mechanisms then:

  1. Options set at an environment variable level will override options
    configured at the SpannerConnection level.
  2. Options set at a query-level will override options set at either the
    SpannerConnection or environment variable level.

If no options are set, the optimizer version will default to:

  1. The optimizer version the database is pinned to.
  2. If the database is not pinned to a specific version, then the Cloud Spanner
    backend will use the "latest" version.

@googlebot googlebot added the cla: yes This human has signed the Contributor License Agreement. label Mar 5, 2020
@skuruppu skuruppu added the api: spanner Issues related to the Spanner API. label Mar 5, 2020
Copy link
Collaborator

@jskeet jskeet left a 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 :)

Copy link
Contributor Author

@skuruppu skuruppu left a comment

Choose a reason for hiding this comment

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

PTAL

@jskeet
Copy link
Collaborator

jskeet commented Mar 6, 2020

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 :)

@skuruppu skuruppu changed the title Spanner qo versioning support feat: Spanner qo versioning support Mar 8, 2020
@skuruppu skuruppu changed the title feat: Spanner qo versioning support feat: [Spanner] Query optimizer versioning support Mar 9, 2020
@skuruppu skuruppu changed the title feat: [Spanner] Query optimizer versioning support feat: [Spanner] Add query options support Mar 9, 2020
Copy link
Collaborator

@jskeet jskeet left a 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();
Copy link
Collaborator

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 };

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.

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.

@@ -84,11 +84,6 @@ public QueryOptions() : this(new V1.ExecuteSqlRequest.Types.QueryOptions())
return new QueryOptions(proto.Clone());
}

/// <summary>
Copy link
Collaborator

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.

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 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.
Comment on lines 183 to 184
// Returning null since we can't return a SpannerClient.ExecuteStreamingSqlStream()
// since it's an abstract class.
Copy link
Contributor Author

@skuruppu skuruppu Mar 11, 2020

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.

Copy link
Collaborator

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.

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

@skuruppu
Copy link
Contributor Author

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.

Copy link
Collaborator

@jskeet jskeet left a 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?

Comment on lines 183 to 184
// Returning null since we can't return a SpannerClient.ExecuteStreamingSqlStream()
// since it's an abstract class.
Copy link
Collaborator

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
Copy link
Collaborator

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)
Copy link
Collaborator

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

Copy link
Contributor Author

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.

Copy link
Contributor Author

@skuruppu skuruppu left a 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 :)

Comment on lines 183 to 184
// Returning null since we can't return a SpannerClient.ExecuteStreamingSqlStream()
// since it's an abstract class.
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 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)
Copy link
Contributor Author

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.

Copy link
Collaborator

@jskeet jskeet left a 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)
Copy link
Collaborator

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?

Copy link
Contributor Author

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]
Copy link
Collaborator

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.

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

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.

Copy link
Contributor Author

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

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 :)

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 :)


private QueryOptions(V1.ExecuteSqlRequest.Types.QueryOptions proto) => Proto = proto;

/// <summary>
Copy link
Collaborator

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?

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 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>
Copy link
Collaborator

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.

Copy link
Contributor Author

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.

Copy link
Collaborator

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

Copy link
Contributor Author

@skuruppu skuruppu left a 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)
Copy link
Contributor Author

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

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";
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 :)


private QueryOptions(V1.ExecuteSqlRequest.Types.QueryOptions proto) => Proto = proto;

/// <summary>
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 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>
Copy link
Contributor Author

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.

@jskeet
Copy link
Collaborator

jskeet commented Mar 13, 2020

Great, thanks for all of that. Are you now happy for me to squash into a single commit and merge?

@skuruppu
Copy link
Contributor Author

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.

@skuruppu skuruppu merged commit c5090d1 into master Mar 14, 2020
@skuruppu
Copy link
Contributor Author

@jskeet I have to ask you about cutting a release with these changes. What process should I follow?

@jskeet
Copy link
Collaborator

jskeet commented Mar 14, 2020

@skuruppu: Will email you internally.

@jskeet jskeet deleted the spanner-qo-versioning-support branch March 30, 2020 11:42
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 Spanner API. cla: yes This human has signed the Contributor License Agreement.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants