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

Extended formatted sql preconditions by table-/viewExists #5456

Merged
merged 17 commits into from
Mar 5, 2024

Conversation

JulienMa94
Copy link
Contributor

@JulienMa94 JulienMa94 commented Jan 11, 2024

Impact

  • Bug fix (non-breaking change which fixes expected existing functionality)
  • Enhancement/New feature (adds functionality without impacting existing logic)
  • Breaking change (fix or feature that would cause existing functionality to change)

Description

As described in (#3767) I extended the formatted sql parsing with a table-/viewExists precondition.

The user can add a tableExist precondition with the following syntax to his changeset.

--changeset "n marcq":"change 3" (stripComments:false splitStatements:false endDelimiter:X runOnChange:true runAlways:true contextFilter:y dbms:mysql runInTransaction:false failOnError:false)
--precondition-table-exists table:table1 schema:12345
create table table2 (
    id int primary key
);

or in the viewExists case like so

--changeset "n marcq":"change 3" (stripComments:false splitStatements:false endDelimiter:X runOnChange:true runAlways:true contextFilter:y dbms:mysql runInTransaction:false failOnError:false)
--precondition-view-exists view:view1 schema:12345'
create table table1 (
    id int primary key
);

As the documentation (https://docs.liquibase.com/concepts/changelogs/preconditions.html) says the parameter tableName / viewName are required and the schemaName is an optional parameter.

For this change some new regex patterns where introduced in the AbstractFormattedChangeLogParser and used inside the FormattedSqlChangeLogParser Class to extract the precondition type & parameters.

Addtionally two groovy tests targeting the parsing logic were added.

Additional Context

Some manuell testing with a postgres db was done to verify the changes on my local setup.

Case 1: Table Exists precondition fails because of a missing table

In the screenshot below we can see that the example.sql contains two changesets. The first creates a regular table. The second changeset tests with a precondition if a table is existing and creates a second table if the precondition passes.
On the right we can see a freshly started postgres database.
table-exists-precondition-fail-before

Precondition test failed because we searched for a non existing table.
Result only one table was created.
table-exists-precondition-fail-after

Case 2: Table Exists precondition successfully passed and runs the changeset

In the screenshot below we can see that the example.sql contains two changesets. The first creates a regular table. The second changeset tests with a precondition if a table is existing and creates a second table if the precondition passes.
On the right we can see a freshly started postgres database.
table-exists-precondition-success-before

Precondition passes because we found a corresponding table.
Result reflects that two tables were created.
table-exists-precondition-success_after

If any other preconditions would be beneficial for the formatted sql syntax, let me know I am happy to contribute some additional conditions.

Fixes #3767

@JulienMa94 JulienMa94 changed the title Extended formatted sql preconditions by table-/viewExists (#3767) #3767 Extended formatted sql preconditions by table-/viewExists Jan 11, 2024
@JulienMa94 JulienMa94 changed the title #3767 Extended formatted sql preconditions by table-/viewExists Extended formatted sql preconditions by table-/viewExists Jan 11, 2024
}

tableExistsPrecondition.setTableName(tableMatcher.group(1));
System.out.println(tableMatcher.group(1));
Copy link
Contributor

Choose a reason for hiding this comment

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

There are a few of these messages in the parseTableExistsCondition() and in parseViewExistsCondition(). I imagine these were added for testing purposes when you developed these methods, if that's the case can we please remove them? But if you add them trying to log status or any specific action, please use a logger and add a message of what you want to track.

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 will remove them. They only were added for some testing purposes.

@MalloD12
Copy link
Contributor

Hi @JulienMa94,

Thank you for creating this PR, I like this enhancement. I think overall code changes look good to me, but performing a bit of manual testing I noticed when trying to deploy a changeset like the one below using a precondition-view-exists clause, but without specifying a view name (which is a required field):

--changeset "n voxland":"change 6" (stripComments:false splitStatements:false endDelimiter:X runOnChange:true runAlways:true contextFilter:y runInTransaction:false failOnError:false)
--precondition-view-exists
create table table4 (
    id int primary key
);

Precondition is not correctly parsed, hence ignored, and change is deployed anyway without returning any error. The same applies to precondition-table-exists.

Can you please take a look at it?

Also, I think it would be nice if we could add some of these negative tests (without specifying required fields or providing null values), I think having a few more scenarios would give us some more confidence.

Thanks,
Daniel.

@JulienMa94
Copy link
Contributor Author

Hi @MalloD12,

thx for your review I will look into this cases as soon I will have some time & add additional test cases 👍🏼

Greetings
Julien

@MalloD12
Copy link
Contributor

Hi @MalloD12,

thx for your review I will look into this cases as soon I will have some time & add additional test cases 👍🏼

Greetings Julien

Sounds good, thank you for all your help!

Cheers,
Daniel.

@JulienMa94
Copy link
Contributor Author

JulienMa94 commented Feb 5, 2024

Hi @MalloD12,

I was able to reproduce the mentioned case above for table-exists, view-exists and also for the existing precondition sqlCheck.
This behaviour occured because no Regex Pattern matched in the AbstractFormattedChangeLogParser.

protected final String PRECONDITION_REGEX = String.format("\\s*%s[\\s]*precondition\\-([a-zA-Z0-9-]+) (.*)", getSingleLineCommentSequence());

The processing of any precondition is ignored at this time when no parameters are provided by the user. Leading to this misleading behaviour.

My suggestion is to enhance the AbstractFormattedChangeLogParser with a regex which captures the invalid regex values and calls an abstract Method handleInvalidEmptyPreconditionCase which is implemented by the FormattedSqlChangeLogParser.

AbstractFormattedChangeLogParser

Abstract Method

protected abstract void handleInvalidEmptyPreconditionCase(ChangeLogParameters changeLogParameters, ChangeSet changeSet, Matcher preconditionMatcher) throws ChangeLogParseException;

Invalid empty precondition regex

protected final String INVALID_EMPTY_PRECONDITION_REGEX = String.format("\\s*%s[\\s]*precondition\\-([a-zA-Z0-9-]+)", getSingleLineCommentSequence());

FormattedSqlChangeLogParser

@Override
    protected void handleInvalidEmptyPreconditionCase(ChangeLogParameters changeLogParameters, ChangeSet changeSet, Matcher preconditionMatcher) throws ChangeLogParseException {
        if (preconditionMatcher.groupCount() == 1) {
            String name = StringUtil.trimToNull(preconditionMatcher.group(1));
            if (name != null) {
                if ("sql-check".equals(name)) {
                    throw new ChangeLogParseException("Precondition sql check failed because of missing required Parameter expectedResult and sql.");
                } else if ("table-exists".equals(name)) {
                    throw new ChangeLogParseException("Precondition table exists failed because of missing required table name parameter.");
                } else if ("view-exists".equals(name)) {
                    throw new ChangeLogParseException("Precondition view exists failed because of missing required view name parameter.");
                } else {
                    throw new ChangeLogParseException("The '" + name + "' precondition type is not supported.");
                }
            }
        }
    }

In this case we handle an empty precondition case by throwing an ChangeLogParseException and inform the user about what parameters were missing.
I added three more tests to handle this case of an empty precondition for sqlCheck, tableExists & viewExists.

Greetings
Julien

@MalloD12
Copy link
Contributor

MalloD12 commented Feb 6, 2024

Hi @MalloD12,

I was able to reproduce the mentioned case above for table-exists, view-exists and also for the existing precondition sqlCheck. This behaviour occured because no Regex Pattern matched in the AbstractFormattedChangeLogParser.

protected final String PRECONDITION_REGEX = String.format("\\s*%s[\\s]*precondition\\-([a-zA-Z0-9-]+) (.*)", getSingleLineCommentSequence());

The processing of any precondition is ignored at this time when no parameters are provided by the user. Leading to this misleading behaviour.

My suggestion is to enhance the AbstractFormattedChangeLogParser with a regex which captures the invalid regex values and calls an abstract Method handleInvalidEmptyPreconditionCase which is implemented by the FormattedSqlChangeLogParser.

AbstractFormattedChangeLogParser

Abstract Method

protected abstract void handleInvalidEmptyPreconditionCase(ChangeLogParameters changeLogParameters, ChangeSet changeSet, Matcher preconditionMatcher) throws ChangeLogParseException;

Invalid empty precondition regex

protected final String INVALID_EMPTY_PRECONDITION_REGEX = String.format("\\s*%s[\\s]*precondition\\-([a-zA-Z0-9-]+)", getSingleLineCommentSequence());

FormattedSqlChangeLogParser

@Override
    protected void handleInvalidEmptyPreconditionCase(ChangeLogParameters changeLogParameters, ChangeSet changeSet, Matcher preconditionMatcher) throws ChangeLogParseException {
        if (preconditionMatcher.groupCount() == 1) {
            String name = StringUtil.trimToNull(preconditionMatcher.group(1));
            if (name != null) {
                if ("sql-check".equals(name)) {
                    throw new ChangeLogParseException("Precondition sql check failed because of missing required Parameter expectedResult and sql.");
                } else if ("table-exists".equals(name)) {
                    throw new ChangeLogParseException("Precondition table exists failed because of missing required table name parameter.");
                } else if ("view-exists".equals(name)) {
                    throw new ChangeLogParseException("Precondition view exists failed because of missing required view name parameter.");
                } else {
                    throw new ChangeLogParseException("The '" + name + "' precondition type is not supported.");
                }
            }
        }
    }

In this case we handle an empty precondition case by throwing an ChangeLogParseException and inform the user about what parameters were missing. I added three more tests to handle this case of an empty precondition for sqlCheck, tableExists & viewExists.

Greetings Julien

Nice, thanks for implementing these new changes. I'll take some time to look into the new changes tomorrow.

Thanks,
Daniel.

Copy link
Contributor

@MalloD12 MalloD12 left a comment

Choose a reason for hiding this comment

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

Approved.

I made a very small change because there was one of the new tests failing. Manually verified all the scenarios are working, so in my opinion we are ready to go with this changes.

Thank you for all your work on this PR.

Daniel.

Copy link
Contributor

@tati-qalified tati-qalified left a comment

Choose a reason for hiding this comment

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

The functionality is correct for databases H2, PostgreSQL and MSSQL. I think it's safe to assume it will work in general.
The print statements are still present, so they should be removed before merging to main.

@MalloD12
Copy link
Contributor

The functionality is correct for databases H2, PostgreSQL and MSSQL. I think it's safe to assume it will work in general. The print statements are still present, so they should be removed before merging to main.

Good point! @JulienMa94 if you don't mind I'll clean them up.

Thanks,
Daniel.

@JulienMa94
Copy link
Contributor Author

The functionality is correct for databases H2, PostgreSQL and MSSQL. I think it's safe to assume it will work in general. The print statements are still present, so they should be removed before merging to main.

Good point! @JulienMa94 if you don't mind I'll clean them up.

Thanks, Daniel.

Thanks Daniel! Sry for forgetting to remove the dev prints. I thought I already had removed them all.

Cheers,
Julien

@filipelautert filipelautert merged commit 18db630 into liquibase:master Mar 5, 2024
30 of 33 checks passed
@filipelautert filipelautert added this to the 1NEXT milestone Mar 5, 2024
@adrian-velonis1
Copy link
Contributor

Internal doc: https://datical.atlassian.net/browse/PD-4389

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

Add precondition support for the SQL Dialect
6 participants