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

test: use liquibase test harness #71

Merged
merged 9 commits into from Mar 8, 2021
Merged

Conversation

olavloite
Copy link
Collaborator

@olavloite olavloite commented Feb 26, 2021

Adds the standard Liquibase test harness suite tests to the Spanner Liquibase integration. The tests can be executed from the IDE (IntelliJ), and works both when the project has been imported as a Maven and as a Gradle project.

  • All tests that cover features that are not supported by Cloud Spanner (e.g. views, dropping primary keys, ...) are skipped.
  • Tests that use tables without primary keys in the default test suite are overridden by a Spanner specific version that does use a primary key.

All tests that cover features that are supported by Cloud Spanner succeed (on the emulator). For this, a couple of functional changes have been made to the Spanner Liquibase integration:

  1. A Spanner-specific AddColumnGenerator has been added to ensure that the statement includes the COLUMN keyword (i.e. ALTER TABLE Foo ADD COLUMN Bar ...`). This is currently only required by the emulator, but as it is to be expected that many users will use the emulator to test their applications, it is important that the integration works as much as possible with the emulator.
  2. A Spanner-specific AddForeignKeyConstraintGenerator has been added to ensure that any OnUpdate and OnDelete specifications are ignored, as this is not supported by Cloud Spanner. This is also in line with the Liquibase behavior for other databases that do not support these keywords.
  3. An extra check was added to the CreateTableGenerator for Spanner. An array-out-of-bounds exception could be thrown if a table without primary keys was being generated. Now it throws an understandable exception, as Cloud Spanner does not support tables without a primary key.

This change also touches a large number of files as it adds the ICloudSpanner marker interface. Using a marker interface instead of a class for checking whether the database in use is Cloud Spanner is better, as it allows us to generate proxy objects when only one small feature of the default Liquibase implementation needs to be adjusted for a specific change. This strategy is applied to the new AddColumnGenerator to add the COLUMN keyword to the generated SQL.

@google-cla google-cla bot added the cla: yes CLA signed label Feb 26, 2021
Copy link
Collaborator

@mescanne mescanne left a comment

Choose a reason for hiding this comment

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

Are we not going to maintain the maven and gradle build separately?

@olavloite
Copy link
Collaborator Author

Are we not going to maintain the maven and gradle build separately?

This is only a draft to discuss with the people from Liquibase on how to get the test harness to work. This PR is green, but I expected it to fail as it should include additional tests that should fail.

@olavloite
Copy link
Collaborator Author

cc @voulg


public class CloudSpanner extends AbstractJdbcDatabase {
public class CloudSpanner extends AbstractJdbcDatabase implements ICloudSpanner {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Adding this interface makes it a lot easier to create proxies of the Cloud Spanner database. This can be very useful when a change type only needs very small adjustments from the standard generator. Instead of copy-pasting the code from the standard generator into the Spanner specific generator, a proxy object can be used to only override specific behavior at that moment. See the new AddColumnGeneratorSpanner for an example, where the only change is that we need it to generate ALTER TABLE ADD COLUMN columnName instead of ALTER TABLE ADD columnName.

if (statement instanceof UpdateStatement) {
UpdateStatement updateStatement = (UpdateStatement) statement;
if (updateStatement.getWhereClause() == null) {
updateStatement.setWhereClause("TRUE");
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Automatically add a WHERE TRUE clause to update statements that fill the initial value of a column.

@@ -36,7 +37,7 @@

@Override
public boolean supports(Database database) {
return (database instanceof CloudSpanner);
return (database instanceof ICloudSpanner);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Checking against ICloudSpanner (the interface) instead of CloudSpanner ensures that changes that generate a proxy for the database are still considered Cloud Spanner databases.


@Override
protected String generateSingleColumnSQL(AddColumnStatement statement, Database database) {
InvocationHandler handler = (proxy, method, args) -> {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Generate a proxy that will add the COLUMN keyword in front of the column name when it is needed specifically for this change.

@olavloite olavloite marked this pull request as ready for review March 5, 2021 12:39
@olavloite
Copy link
Collaborator Author

@mescanne Would you mind taking a second look? This should now work for both Gradle and Maven. Note that the Liquibase harness test suite can (currently) only be executed in IntelliJ, and that you must use Java 8 as the execution environment.

@voulg and @kristyldatical Please feel free to add any comments / questions you might have as well.

Copy link
Collaborator

@mescanne mescanne left a comment

Choose a reason for hiding this comment

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

Thorough and great to be integrated with Liquibase's test harness. Some comments in there I'd be interested in your take on.

ICloudSpanner databaseWithColumnKeyword = (ICloudSpanner) Proxy.newProxyInstance(ICloudSpanner.class.getClassLoader(),
new Class[] { ICloudSpanner.class },
handler);
return super.generateSingleColumnSQL(statement, databaseWithColumnKeyword);
Copy link
Collaborator

Choose a reason for hiding this comment

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

It looks like the proxied Database is only scoped to generateSingleColumnSQL. I believe this addition of COLUMN is only needed for the emulator but not real Spanner -- I did start looking into changing the emulator itself, which I think was acceptable.

We can use the proxy in the meantime, but I'd have a strong preference to use it in the long term.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It looks like the proxied Database is only scoped to generateSingleColumnSQL.

Correct, but the generateMultipleColumns method will just call generateSingleColumnSQL multiple times, so it covers that scenario as well.

I agree that the preference would be to not need the additional COLUMN keyword, as it is indeed only needed for the emulator. Once that is fixed, we can remove this.

@@ -16,6 +16,7 @@
import liquibase.database.Database;
import liquibase.exception.ValidationErrors;
import liquibase.ext.spanner.CloudSpanner;
Copy link
Collaborator

Choose a reason for hiding this comment

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

We can remove the import of liquibase.ext.spanner.CloudSpanner as well.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good point, done.


-- To run Liquibase Harness test suite:
-- 1. Start Cloud Spanner emulator with `docker run -p 9010:9010 -p 9020:9020 gcr.io/cloud-spanner-emulator/emulator`
-- 2. Execute the script below to initialize a database on projects/my-project/instances/my-instance/databases/my-database
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does the emulator automatically created my-instance and my-database? That was always a challenge using the emulator for me.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah, yeah, that's a good point. I currently have a locally built version of the JDBC driver that does that :-) So I forgot to put in the documentation that you need to do that manually for the time being. Once this PR has been merged and a new version has been released, it will no longer be necessary.

@@ -52,7 +52,7 @@
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

<liquibase.version>3.8.9</liquibase.version>
<liquibase.version>4.3.1</liquibase.version>
Copy link
Collaborator

Choose a reason for hiding this comment

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

Gradle build uses 4.2.0. Is it worth maintaining Gradle separately from Maven? We can look to drop Maven -- it would be better from a CI perspective to use a single build system.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, I would also prefer switching to just one. The only thing I (or someone else) has to configure is the release process to Maven central in the Gradle setup. That is currently only setup in the pom.
I'll look into that in a separate PR.

database.escapeColumnNameList(
StringUtils.join(statement.getPrimaryKeyConstraint().getColumns(), ", ")));
buffer.append(")");
if (statement.getPrimaryKeyConstraint() != null && statement.getPrimaryKeyConstraint().getColumns() != null) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Pure style (and I'm not a Google style expert) question. Wouldn't this be preferred -->
// If no PK constraint, leave it as it is and let Spanner itself throw the error
if (statement.getPrimaryKeyConstraint() == null || statement.getPrimaryKeyConstraint().getColumns() == null) {
return res;
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The (open source) style guide does not explicitly state so, but I agree that in this case it's certainly more readable.

Copy link
Collaborator

@mescanne mescanne 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 - still want to get the emulator fixed, and the new JDBC auto-create-instance&database looks good, too.

@olavloite olavloite merged commit c4e9eba into master Mar 8, 2021
@olavloite olavloite deleted the liquibase-test-harness branch March 8, 2021 06:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cla: yes CLA signed
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants