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: support Savepoint #1212

Merged
merged 3 commits into from May 26, 2023
Merged

feat: support Savepoint #1212

merged 3 commits into from May 26, 2023

Conversation

olavloite
Copy link
Collaborator

Add support for emulated Savepoints that are now supported in the client library.

Add support for emulated Savepoints that are now supported in the client
library.
@olavloite olavloite requested review from a team as code owners April 25, 2023 19:03
@product-auto-label product-auto-label bot added size: l Pull request size is large. api: spanner Issues related to the googleapis/java-spanner-jdbc API. labels Apr 25, 2023
Copy link
Contributor

@thiagotnunes thiagotnunes left a comment

Choose a reason for hiding this comment

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

LGTM

class JdbcSavepoint implements Savepoint {
private static final AtomicInteger COUNTER = new AtomicInteger();

static JdbcSavepoint named(String name) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Any reason we are preferring static methods for instantiation instead of adding constructors for creating named and unnamed instances? Wouldn't the constructor be a more natural way for creating instance instead of static methods?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This gives more readable code where the savepoints are created, especially as the behavior of named and unnamed savepoints are slightly different. Internally, we always need to create savepoints with a name, as that is the only thing that Cloud Spanner supports. This way we make clear that one is a named savepoint, and the other is unnamed, even though both internally have a name.


@Override
public int getSavepointId() throws SQLException {
JdbcPreconditions.checkState(this.id >= 0, "This is a named savepoint");
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggestion : It might be more readable to track named vs un-named through an explicit boolean variable say isNamed . Using the the ID's state to determine this seems like an over-use of ID's value beyond what its meant for. For example - If for any reason we change this ID's type from integer to String, it ideally shouldn't impact the logic to determine named vs un-named. But in this case we will need to modify the logic.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don't feel it's really necessary given the size of the class. Also; changing the type of ID from integer to anything else will not be possible, as the type is specified by the JDBC API (that is; getSavepointId must return an int).

@@ -402,6 +404,69 @@ public String getSchema() throws SQLException {
return "";
}

@Override
public SavepointSupport getSavepointSupport() {
return getSpannerConnection().getSavepointSupport();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Have we intentionally skipped checkClosed(); validation in this method? If yes, I didn't get why we won't need the check here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Not really intentionally. Added it to be consistent with the other methods.

return name;
}

String internalGetSavepointName() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

  1. Why is this method being added to an implementation when the interface Savepoint is not defining any such method?
  2. Why are we prefixing this with internal ? Wouldn't it be better to call this getName which acts like a getter method for existing member name?
  3. Why are we adding this method when there is already getSavepointName() method?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We need a separate method from getSavepointName() because:

  1. JDBC specifies that getSavepointName() must throw an error if it is called on an unnamed savepoint.
  2. We internally need to get the name of a savepoint in order to set it on the underlying connection. For an unnamed savepoint, this will be a generated name that is only accessible internally in the package, and not to the outside world.
  3. internalGetSavepointName() is the internal (package-private) version of the getSavepointName() method that returns the name regardless whether the savepoint is named or unnamed. This is used internally to set the savepoint name on the underlying connection.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks for the explanation.

public Savepoint setSavepoint() throws SQLException {
checkClosed();
try {
JdbcSavepoint savepoint = JdbcSavepoint.unnamed();
Copy link
Collaborator

Choose a reason for hiding this comment

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

What's your take on inversion of control ? Are you intentionally using JdbcSavepoint since you require the static methods and the internalGetSavepointName() method?

Usually with interfaces we don't expose the implementation class at time of dependency injection. Over here it's not really dependency injection, but would it not be better to use just Savepoint interface instead of referencing JdbcSavepoint class?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We need to use the JdbcSavepoint class instead of the Savepoint interface here, because we need access to the internalGetSavepointName() method. We cannot use the Savepoint#getSavepointName() method, because that method will throw an error for unnamed savepoints. The 'throw-an-error' behavior is part of the JDBC specification, so it is not something that we can choose to implement differently.

Comment on lines +450 to +452
JdbcSavepoint jdbcSavepoint = (JdbcSavepoint) savepoint;
try {
getSpannerConnection().rollbackToSavepoint(jdbcSavepoint.internalGetSavepointName());
Copy link
Collaborator

Choose a reason for hiding this comment

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

Similar comments on usage of JdbcSavepoint class instead of Savepoint interface as above methods.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Same as above: We need access to the internalGetSavepointName() method, as the Savepoint#getSavepointName() could throw an error if the given savepoint is an unnamed savepoint.

@olavloite olavloite merged commit 6833696 into main May 26, 2023
21 checks passed
@olavloite olavloite deleted the savepoint branch May 26, 2023 14:20
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 googleapis/java-spanner-jdbc API. size: l Pull request size is large.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants