Skip to content

Add CREATE TRIGGER support for SQL Server #1810

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

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

aharpervc
Copy link
Contributor

@aharpervc aharpervc commented Apr 11, 2025

Adjacent to: #1808 with similar considerations, and temporarily rebased on it (eg, this branch should probably wait for that branch to merge).


This PR introduces support for parsing CREATE TRIGGER for SQL Server.

The main concern is that for the existing dialects, there was an expectation of an EXECUTE keyword (PG: https://www.postgresql.org/docs/current/sql-createtrigger.html). However, SQL Server doesn't use this syntax and instead supports multi statement blocks (like a stored procedure).

The difficulty here in the codebase is what to do about CreateTrigger.exec_body & TriggerExecBody. In this iteration I made the property an Option, which seemed like the least impact on existing code. TriggerExecBody, etc can be left alone.

However in the future I think this could be improved with a more broad refactoring, such as a TriggerBody enum, which can consolidate TriggerExecBodyType's options & use Vec<Statements> for a MultiStatement variant and FunctionDesc for Function & Procedure variants. That'd basically remove the TriggerExecBody struct, too. Overall I think that'd be a cleaner approach for the CreateTrigger struct.


That's all speculative followup work, for now I just want to be able to parse most SQL Server triggers. Other SQL Server specific things like particular trigger options, DDL trigger stuff, etc can come later as needed.

@aharpervc aharpervc force-pushed the mssql-create-trigger branch from 39517ce to b94916f Compare April 11, 2025 21:30
@aharpervc aharpervc marked this pull request as ready for review April 11, 2025 22:00
@aharpervc aharpervc force-pushed the mssql-create-trigger branch from b94916f to bd6d624 Compare April 14, 2025 16:13
@aharpervc
Copy link
Contributor Author

aharpervc commented Apr 14, 2025

This fails to parse on this branch, but should:

CREATE TRIGGER some_trigger ON some_table FOR INSERT
AS
BEGIN
    IF 1=1
    BEGIN
        RAISERROR('Trigger fired', 10, 1);
    END

    RETURN;
END

Fixed, never mind

@aharpervc
Copy link
Contributor Author

FYI for reviewers I rebased this on the create function branch, due to the return statement logic here: #1808 (comment)

@aharpervc aharpervc force-pushed the mssql-create-trigger branch from bd6d624 to aaa2ab3 Compare April 14, 2025 19:53
@aharpervc aharpervc force-pushed the mssql-create-trigger branch 6 times, most recently from 77776fc to d5d376e Compare April 21, 2025 18:54
@aharpervc aharpervc force-pushed the mssql-create-trigger branch from d5d376e to d2b564d Compare April 23, 2025 19:41
@aharpervc
Copy link
Contributor Author

Rebased again now that #1808 has been merged. Should be ready for review.

Comment on lines 5268 to 5270
if dialect_of!(self is MsSqlDialect) {
return self.parse_mssql_create_trigger(or_alter, or_replace, is_constraint);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe this could be parsed via self.dialect.parse_statement() for the mssql version? since the dialect_of macro we try to stay away for new code

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 seems tricky. Can you help me understand what that code would look like? parse_create_trigger is happening inside a complicated branch of parse_create, so it seems like that logic from src/parser/mod.rs would need to be copy/pasted to src/dialect/mssql.rs which seems unfortunate.

Otherwise I don't know how to have the dialect's parse_statement implement custom logic only for "create trigger" but not for all the other "create xxx"'s

Copy link
Contributor

Choose a reason for hiding this comment

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

ah yeah it would need to manually parse the alter, replace for the trigger, I agree that its not ideal but its probably the lesser evil, see snowflake here for an example

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Okay, thanks for the reference. The patterns in this library are difficult to interpret sometimes (what's best practice vs what's deprecated). I've moved the trigger parsing logic into the dialect. Doing it this way also means we need to be able to rewind the parser if it's some form of "create" not handled particularly in the dialect, so I also added a parser method to reset the index in that case accordingly.

@aharpervc aharpervc force-pushed the mssql-create-trigger branch 3 times, most recently from 1f80e42 to d042e88 Compare April 29, 2025 18:28
let original_index = parser.index();

if !parser.parse_keyword(Keyword::CREATE) {
parser.set_index(original_index);
Copy link
Contributor

Choose a reason for hiding this comment

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

I think instead of manually tracking index we can rely on the parser.maybe_parse()? that will automatically handle the indexing

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've added a commit to the branch for this approach.

I'm not sure it's strictly better because it seems more difficult to extend if/when additional CREATE things are moved into dialects, if this is a general pattern. Perhaps you can clear up what I'm concerned about here -- with maybe_parse if parsing fails inside the dialect, it'll rewind the parser & report None up the chain. Then, parsing will continue as if it was common code. However that might not be what we prefer -- if we're already into parse_create_trigger and get a parsing error, we actually do want it to fail right there rather than rewinding. In the former approach that seemed clear. With this approach it seems like the dialect specific parser failure will get ignored (and you will probably get another parser error from the general code instead).

Thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah yes you're right, if failure occurs while parsing the trigger body for example the ideal behavior in this case would be to report that error instead of returning None. In which case maybe_parse isn't ideal.

I think the main goal is avoiding a custom implementation to track indexing here, looking at the new diff it seems the sqlserver version is simpler so maybe something like this suffices already?

if self.parse_keywords(CREATE, TRIGGER) {
  Some(self.parse_create_trigger(self, parser, false))
} else if self.parse_keywords(CREATE, OR, ALTER, TRIGGER) {
  Some(self.parse_create_trigger(self, parser, true))
} else {
  None
}

Copy link
Contributor Author

@aharpervc aharpervc Apr 30, 2025

Choose a reason for hiding this comment

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

Great, I'll drop the maybe_parse commit & update the implementation to this two branch approach. I think that will meet both our expectations for the time being.

A pro/con on that iteration will be that the dialect implementation of parse_statement is mixing peek & parse strategies in that function, which seems unfortunate. However further refactoring it to consolidate strategies seems outside the scope of the branch.

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 👍

@aharpervc aharpervc requested a review from iffyio April 29, 2025 22:04
@aharpervc aharpervc force-pushed the mssql-create-trigger branch from 144c049 to dc7eef0 Compare April 30, 2025 14:08

parser.expect_keyword_is(Keyword::AS)?;

let trigger_statements_body = if parser.peek_keyword(Keyword::BEGIN) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Similar approach in the other PR for stored procedures that may/not have begin/end tokens https://github.com/apache/datafusion-sqlparser-rs/pull/1834/files#r2069458988

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 also updated the related stored procedure PR accordingly: #1834 (comment)

@aharpervc aharpervc force-pushed the mssql-create-trigger branch from 23a063f to 06ae263 Compare May 1, 2025 14:52
Comment on lines 249 to 257
BeginEndStatements {
begin_token: AttachedToken(begin_token),
statements,
end_token: AttachedToken(end_token),
}
} else {
BeginEndStatements {
begin_token: AttachedToken::empty(),
statements: parser.parse_statements()?,
end_token: AttachedToken::empty(),
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Oh I think instead of representing the begin optionality using tokens, we can use an enum, something similar to ConditionalStatements where we have an explicit BeginEnd enum variant

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Gotcha, yeah I could see it either way. I will update this to use the enum pattern

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 👍

aharpervc added 2 commits May 2, 2025 10:59
- similar to functions & procedures, this dialect can define triggers with a multi statement block
- there's no `EXECUTE` keyword here, so that means the `exec_body` used by other dialects becomes an `Option`, and our `statements` is also optional for them
@aharpervc aharpervc force-pushed the mssql-create-trigger branch from 06ae263 to 000fe3f Compare May 2, 2025 15:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants