From 983666d9b7f7c8f2cf1c8c837de7c58b6613f97c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Go=C5=82embiewski?= Date: Thu, 24 Sep 2026 20:40:24 +0200 Subject: [PATCH] feat(mdl): DROP IF EXISTS (#1190) A script that dropped a document ran once: the second run stopped at the DROP and skipped every later statement. ENTITY, ASSOCIATION, ENUMERATION, CONSTANT, MICROFLOW, NANOFLOW, PAGE, LAYOUT, SNIPPET, MENU, JAVA ACTION and IMAGE COLLECTION now take IF EXISTS, the same ifExists rule DROP USER ROLE uses. Registry.Dispatch turns a not-found for the named document (or its module) into " does not exist, skipping"; any other error still fails. --- .claude/skills/mendix/write-layouts/SKILL.md | 8 ++- CHANGELOG.md | 4 ++ cmd/mxcli/syntax/features_page.go | 5 +- .../bug-tests/drop-document-if-exists.mdl | 25 +++++++++ mdl/ast/ast_association.go | 1 + mdl/ast/ast_drop_if_exists.go | 18 +++++++ mdl/ast/ast_entity.go | 1 + mdl/ast/ast_enumeration.go | 2 + mdl/ast/ast_imagecollection.go | 1 + mdl/ast/ast_javaaction.go | 1 + mdl/ast/ast_microflow.go | 2 + mdl/ast/ast_navigation.go | 1 + mdl/ast/ast_page.go | 3 ++ mdl/executor/registry.go | 29 +++++++++- mdl/executor/registry_skip_missing_test.go | 54 +++++++++++++++++++ mdl/executor/validate_duplicates.go | 2 + mdl/grammar/MDLParser.g4 | 28 +++++----- mdl/visitor/drop_if_exists_test.go | 31 +++++++++++ mdl/visitor/visitor_entity.go | 36 ++++++++----- 19 files changed, 225 insertions(+), 27 deletions(-) create mode 100644 mdl-examples/bug-tests/drop-document-if-exists.mdl create mode 100644 mdl/ast/ast_drop_if_exists.go create mode 100644 mdl/executor/registry_skip_missing_test.go diff --git a/.claude/skills/mendix/write-layouts/SKILL.md b/.claude/skills/mendix/write-layouts/SKILL.md index 614cc1d59..ed3a6172c 100644 --- a/.claude/skills/mendix/write-layouts/SKILL.md +++ b/.claude/skills/mendix/write-layouts/SKILL.md @@ -55,9 +55,15 @@ layout, so repoint them first: ```sql ALTER PAGES SET LAYOUT = MyModule.App_New WHERE LAYOUT = MyModule.App_Old; -DROP LAYOUT MyModule.App_Old; +DROP LAYOUT IF EXISTS MyModule.App_Old; ``` +`IF EXISTS` keeps the script re-runnable: on a second run the layout is already +gone, and without it exec stops at the `DROP` and skips everything after it. +Every document DROP a script typically needs takes it — entity, association, +enumeration, constant, microflow, nanoflow, page, layout, snippet, menu, java +action, image collection. + To *correct* a layout rather than remove it, re-create it under the same name (`CREATE OR REPLACE LAYOUT`): the pages stay bound by qualified name and rebind to the new document — verified end to end, the pages go back to 0 errors. diff --git a/CHANGELOG.md b/CHANGELOG.md index cc8b6e66b..338d8fb75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +### Added + +- **`DROP IF EXISTS`** (mendixlabs/mxcli#1190) — `DROP ENTITY`, `ASSOCIATION`, `ENUMERATION`, `CONSTANT`, `MICROFLOW`, `NANOFLOW`, `PAGE`, `LAYOUT`, `SNIPPET`, `MENU`, `JAVA ACTION` and `IMAGE COLLECTION` take `IF EXISTS`: a missing document is reported as `… does not exist, skipping` instead of stopping exec, so a script that removes one can run twice. Only the named document (or its module) being absent is skipped; every other error still fails the statement. + ## [0.24.0] - 2026-09-24 Headline: **An element's storage GUID is the database's identity, and mxcli now treats it as one.** A production report of 28 attributes emptied across 607 rows by a single edit (mendixlabs/mxcli#1119) traced to five write paths that re-minted GUIDs — one of them moving 282 in a single module. They are fixed, and a new guard at the write choke point refuses any write that moves one: a class of data loss that leaves the model valid, `mx check` clean and `DESCRIBE` byte-identical, and surfaces only when the package meets a database that already holds data. Alongside it, `MOVE ENTITY` and `RENAME` stop leaving a project unbuildable, and four more scripts that passed every gate and failed the build are refused. diff --git a/cmd/mxcli/syntax/features_page.go b/cmd/mxcli/syntax/features_page.go index 3b1b8602d..961fdb5aa 100644 --- a/cmd/mxcli/syntax/features_page.go +++ b/cmd/mxcli/syntax/features_page.go @@ -512,7 +512,10 @@ CREATE PAGE Sales.Detail (Title: 'Detail', Layout: Atlas_Core.Atlas_Default) { Keywords: []string{ "drop layout", "delete layout", "remove layout", }, - Syntax: "DROP LAYOUT Module.Name", + Syntax: "DROP LAYOUT [IF EXISTS] Module.Name\n\n" + + "-- IF EXISTS skips a layout that is already gone, so the script re-runs.\n" + + "-- The same clause works on DROP ENTITY, ASSOCIATION, ENUMERATION, CONSTANT,\n" + + "-- MICROFLOW, NANOFLOW, PAGE, SNIPPET, MENU, JAVA ACTION and IMAGE COLLECTION.", Example: "DROP LAYOUT MyModule.App_Old;\n\n" + "-- Pages still bound to it are named in a warning, and the drop proceeds.\n" + "-- Left dropped, each of those pages fails the build with CE1613 — which\n" + diff --git a/mdl-examples/bug-tests/drop-document-if-exists.mdl b/mdl-examples/bug-tests/drop-document-if-exists.mdl new file mode 100644 index 000000000..94b8ac7fe --- /dev/null +++ b/mdl-examples/bug-tests/drop-document-if-exists.mdl @@ -0,0 +1,25 @@ +-- ============================================================================ +-- mendixlabs/mxcli#1190 — DROP IF EXISTS makes a cleanup re-runnable +-- ============================================================================ +-- +-- The second DROP of each pair finds nothing and is skipped with +-- " does not exist, skipping" instead of stopping exec, so this +-- whole file can be executed twice against the same project. +-- ============================================================================ + +CREATE MODULE BugDropIfExists; + +CREATE OR MODIFY PERSISTENT ENTITY BugDropIfExists.Scratch ( Note: String(100) ); + +CREATE OR MODIFY MICROFLOW BugDropIfExists.ACT_Scratch () +BEGIN + LOG INFO 'scratch'; +END; +/ + +DROP MICROFLOW IF EXISTS BugDropIfExists.ACT_Scratch; +DROP MICROFLOW IF EXISTS BugDropIfExists.ACT_Scratch; +DROP ENTITY IF EXISTS BugDropIfExists.Scratch; +DROP ENTITY IF EXISTS BugDropIfExists.Scratch; +DROP LAYOUT IF EXISTS BugDropIfExists.NeverCreated; +DROP PAGE IF EXISTS BugDropIfExists.NeverCreated; diff --git a/mdl/ast/ast_association.go b/mdl/ast/ast_association.go index 0dfa7e8c9..82d65ce26 100644 --- a/mdl/ast/ast_association.go +++ b/mdl/ast/ast_association.go @@ -132,6 +132,7 @@ func (s *CreateAssociationStmt) isStatement() {} // DropAssociationStmt represents: DROP ASSOCIATION Module.Name type DropAssociationStmt struct { Name QualifiedName + DropIfExists } func (s *DropAssociationStmt) isStatement() {} diff --git a/mdl/ast/ast_drop_if_exists.go b/mdl/ast/ast_drop_if_exists.go new file mode 100644 index 000000000..ca9ee5a56 --- /dev/null +++ b/mdl/ast/ast_drop_if_exists.go @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 + +package ast + +// DropIfExists is embedded in the document DROP statements that accept +// IF EXISTS. A missing document is then skipped rather than an error, so a +// script that removes one can run twice (mendixlabs/mxcli#1190). +type DropIfExists struct { + IfExists bool +} + +// SkipsMissing reports whether the statement was written with IF EXISTS. +func (d DropIfExists) SkipsMissing() bool { return d.IfExists } + +// MissingSkipper is implemented by every statement that embeds DropIfExists. +type MissingSkipper interface { + SkipsMissing() bool +} diff --git a/mdl/ast/ast_entity.go b/mdl/ast/ast_entity.go index 2e2ef405e..aae9d4acf 100644 --- a/mdl/ast/ast_entity.go +++ b/mdl/ast/ast_entity.go @@ -58,6 +58,7 @@ func (s *CreateEntityStmt) isStatement() {} // DropEntityStmt represents: DROP ENTITY Module.Name type DropEntityStmt struct { Name QualifiedName + DropIfExists } func (s *DropEntityStmt) isStatement() {} diff --git a/mdl/ast/ast_enumeration.go b/mdl/ast/ast_enumeration.go index e46c780a5..9a7292da4 100644 --- a/mdl/ast/ast_enumeration.go +++ b/mdl/ast/ast_enumeration.go @@ -91,6 +91,7 @@ const ( // DropEnumerationStmt represents: DROP ENUMERATION Module.Name type DropEnumerationStmt struct { Name QualifiedName + DropIfExists } func (s *DropEnumerationStmt) isStatement() {} @@ -117,6 +118,7 @@ func (s *CreateConstantStmt) isStatement() {} // DropConstantStmt represents: DROP CONSTANT Module.Name type DropConstantStmt struct { Name QualifiedName + DropIfExists } func (s *DropConstantStmt) isStatement() {} diff --git a/mdl/ast/ast_imagecollection.go b/mdl/ast/ast_imagecollection.go index 41451cfcb..ea4ddfa0f 100644 --- a/mdl/ast/ast_imagecollection.go +++ b/mdl/ast/ast_imagecollection.go @@ -26,6 +26,7 @@ func (s *CreateImageCollectionStmt) isStatement() {} // DropImageCollectionStmt represents: DROP IMAGE COLLECTION Module.Name type DropImageCollectionStmt struct { Name QualifiedName + DropIfExists } func (s *DropImageCollectionStmt) isStatement() {} diff --git a/mdl/ast/ast_javaaction.go b/mdl/ast/ast_javaaction.go index 16677a3b5..5c7e3b201 100644 --- a/mdl/ast/ast_javaaction.go +++ b/mdl/ast/ast_javaaction.go @@ -47,6 +47,7 @@ func (s *CreateJavaActionStmt) isStatement() {} // DropJavaActionStmt represents: DROP JAVA ACTION Module.Name type DropJavaActionStmt struct { Name QualifiedName + DropIfExists } func (s *DropJavaActionStmt) isStatement() {} diff --git a/mdl/ast/ast_microflow.go b/mdl/ast/ast_microflow.go index 35bd0f0ad..b49dcf112 100644 --- a/mdl/ast/ast_microflow.go +++ b/mdl/ast/ast_microflow.go @@ -146,6 +146,7 @@ func (s *CreateMicroflowStmt) isStatement() {} // DropMicroflowStmt represents: DROP MICROFLOW Module.Name type DropMicroflowStmt struct { Name QualifiedName + DropIfExists } func (s *DropMicroflowStmt) isStatement() {} @@ -214,6 +215,7 @@ func (s *DropRuleStmt) isStatement() {} // DropNanoflowStmt represents: DROP NANOFLOW Module.Name type DropNanoflowStmt struct { Name QualifiedName + DropIfExists } func (s *DropNanoflowStmt) isStatement() {} diff --git a/mdl/ast/ast_navigation.go b/mdl/ast/ast_navigation.go index 352508efd..d9aa86ddf 100644 --- a/mdl/ast/ast_navigation.go +++ b/mdl/ast/ast_navigation.go @@ -86,6 +86,7 @@ func (s *CreateMenuStmt) isStatement() {} // DropMenuStmt is `drop menu Module.Name`. type DropMenuStmt struct { Name QualifiedName + DropIfExists } func (s *DropMenuStmt) isStatement() {} diff --git a/mdl/ast/ast_page.go b/mdl/ast/ast_page.go index 65bdd58cc..894ddbf9c 100644 --- a/mdl/ast/ast_page.go +++ b/mdl/ast/ast_page.go @@ -46,6 +46,7 @@ type DataGridColumnDef struct { // DropPageStmt represents: DROP PAGE Module.Name type DropPageStmt struct { Name QualifiedName + DropIfExists } func (s *DropPageStmt) isStatement() {} @@ -57,6 +58,7 @@ func (s *DropPageStmt) isStatement() {} // exactly that — had no headless remedy at all. type DropLayoutStmt struct { Name QualifiedName + DropIfExists } func (s *DropLayoutStmt) isStatement() {} @@ -64,6 +66,7 @@ func (s *DropLayoutStmt) isStatement() {} // DropSnippetStmt represents: DROP SNIPPET Module.Name type DropSnippetStmt struct { Name QualifiedName + DropIfExists } func (s *DropSnippetStmt) isStatement() {} diff --git a/mdl/executor/registry.go b/mdl/executor/registry.go index 53a3895a1..708bd63d5 100644 --- a/mdl/executor/registry.go +++ b/mdl/executor/registry.go @@ -3,8 +3,10 @@ package executor import ( + "errors" "fmt" "reflect" + "strings" "github.com/mendixlabs/mxcli/mdl/ast" mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" @@ -88,7 +90,32 @@ func (r *Registry) Dispatch(ctx *ExecContext, stmt ast.Statement) error { if h == nil { return mdlerrors.NewUnsupported(fmt.Sprintf("unhandled statement type %T", stmt)) } - return h(ctx, stmt) + return skipMissingIfAsked(ctx, stmt, h(ctx, stmt)) +} + +// skipMissingIfAsked turns " not found" into a notice for a DROP written +// with IF EXISTS. Only the document the statement names counts (or its module, +// which leaves the document missing just the same): any other not-found error +// is a real failure and is returned as is. +func skipMissingIfAsked(ctx *ExecContext, stmt ast.Statement, err error) error { + skipper, ok := stmt.(ast.MissingSkipper) + if err == nil || !ok || !skipper.SkipsMissing() { + return err + } + var missing *mdlerrors.NotFoundError + if !errors.As(err, &missing) { + return err + } + kind, name := stmtDropInfo(stmt) + if missing.Kind != "module" && missing.Name != name { + return err + } + label := strings.ReplaceAll(kind, "-", " ") + if kind == "javaaction" { + label = "java action" + } + fmt.Fprintf(ctx.Output, "%s %s does not exist, skipping\n", label, name) + return nil } // Validate checks that every known AST statement type has a registered diff --git a/mdl/executor/registry_skip_missing_test.go b/mdl/executor/registry_skip_missing_test.go new file mode 100644 index 000000000..06b72974a --- /dev/null +++ b/mdl/executor/registry_skip_missing_test.go @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Apache-2.0 + +package executor + +import ( + "bytes" + "context" + "errors" + "strings" + "testing" + + "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" +) + +// DROP ... IF EXISTS skips only the document it names being missing (or its +// module); every other error still fails the statement (mendixlabs/mxcli#1190). +func TestSkipMissingIfAsked(t *testing.T) { + layout := func(ifExists bool) ast.Statement { + return &ast.DropLayoutStmt{Name: ast.QualifiedName{Module: "M", Name: "Shell"}, + DropIfExists: ast.DropIfExists{IfExists: ifExists}} + } + for _, tc := range []struct { + name string + stmt ast.Statement + err error + wantErr bool + wantNote string + }{ + {"named layout missing", layout(true), mdlerrors.NewNotFound("layout", "M.Shell"), false, "layout M.Shell does not exist, skipping"}, + {"module missing", layout(true), mdlerrors.NewNotFound("module", "M"), false, "layout M.Shell does not exist, skipping"}, + {"without IF EXISTS", layout(false), mdlerrors.NewNotFound("layout", "M.Shell"), true, ""}, + {"another document missing", layout(true), mdlerrors.NewNotFound("page", "M.Other"), true, ""}, + {"not a not-found", layout(true), errors.New("write failed"), true, ""}, + {"success", layout(true), nil, false, ""}, + {"java action label", &ast.DropJavaActionStmt{Name: ast.QualifiedName{Module: "M", Name: "Act"}, + DropIfExists: ast.DropIfExists{IfExists: true}}, mdlerrors.NewNotFound("java action", "M.Act"), false, + "java action M.Act does not exist, skipping"}, + } { + ctx := (&Executor{}).newExecContext(context.Background()) + var out bytes.Buffer + ctx.Output = &out + err := skipMissingIfAsked(ctx, tc.stmt, tc.err) + if (err != nil) != tc.wantErr { + t.Errorf("%s: err = %v, wantErr %v", tc.name, err, tc.wantErr) + } + if tc.wantNote != "" && !strings.Contains(out.String(), tc.wantNote) { + t.Errorf("%s: output = %q, want %q", tc.name, out.String(), tc.wantNote) + } + if tc.wantNote == "" && out.Len() > 0 { + t.Errorf("%s: unexpected output %q", tc.name, out.String()) + } + } +} diff --git a/mdl/executor/validate_duplicates.go b/mdl/executor/validate_duplicates.go index 77c77ead2..b9a62c8cf 100644 --- a/mdl/executor/validate_duplicates.go +++ b/mdl/executor/validate_duplicates.go @@ -209,6 +209,8 @@ func stmtDropInfo(stmt ast.Statement) (docType, name string) { return "agent", s.Name.String() case *ast.DropImageCollectionStmt: return "image-collection", s.Name.String() + case *ast.DropMenuStmt: + return "menu", s.Name.String() } return "", "" } diff --git a/mdl/grammar/MDLParser.g4 b/mdl/grammar/MDLParser.g4 index 69048e9a7..652a7fb24 100644 --- a/mdl/grammar/MDLParser.g4 +++ b/mdl/grammar/MDLParser.g4 @@ -467,30 +467,34 @@ createMenuStatement : MENU_KW qualifiedName (FOLDER STRING_LITERAL)? LPAREN navMenuItemDef* RPAREN ; +// IF EXISTS on the document kinds a script actually removes, so a script that +// drops a document can run twice: without it the second run stops at the DROP +// and every later statement is skipped (mendixlabs/mxcli#1190). Same ifExists +// rule as DROP USER ROLE and ALTER ENTITY ... DROP ATTRIBUTE. dropStatement - : DROP ENTITY qualifiedName - | DROP ASSOCIATION qualifiedName - | DROP ENUMERATION qualifiedName - | DROP CONSTANT qualifiedName - | DROP MICROFLOW qualifiedName - | DROP NANOFLOW qualifiedName + : DROP ENTITY ifExists? qualifiedName + | DROP ASSOCIATION ifExists? qualifiedName + | DROP ENUMERATION ifExists? qualifiedName + | DROP CONSTANT ifExists? qualifiedName + | DROP MICROFLOW ifExists? qualifiedName + | DROP NANOFLOW ifExists? qualifiedName | DROP RULE qualifiedName - | DROP PAGE qualifiedName - | DROP LAYOUT qualifiedName - | DROP SNIPPET qualifiedName - | DROP MENU_KW qualifiedName + | DROP PAGE ifExists? qualifiedName + | DROP LAYOUT ifExists? qualifiedName + | DROP SNIPPET ifExists? qualifiedName + | DROP MENU_KW ifExists? qualifiedName | DROP MODULE qualifiedName | DROP QUEUE qualifiedName | DROP SCHEDULED EVENT qualifiedName | DROP REGULAR EXPRESSION qualifiedName - | DROP JAVA ACTION qualifiedName + | DROP JAVA ACTION ifExists? qualifiedName | DROP JAVASCRIPT ACTION qualifiedName | DROP INDEX qualifiedName ON qualifiedName | DROP ODATA CLIENT qualifiedName | DROP ODATA SERVICE qualifiedName | DROP BUSINESS EVENT SERVICE qualifiedName | DROP WORKFLOW qualifiedName - | DROP IMAGE COLLECTION qualifiedName + | DROP IMAGE COLLECTION ifExists? qualifiedName | DROP ANNOTATION STRING_LITERAL IN identifierOrKeyword | DROP ANNOTATION AT_KW LPAREN NUMBER_LITERAL COMMA NUMBER_LITERAL RPAREN IN identifierOrKeyword | DROP JSON STRUCTURE qualifiedName diff --git a/mdl/visitor/drop_if_exists_test.go b/mdl/visitor/drop_if_exists_test.go index 2c92beef0..54aeb1241 100644 --- a/mdl/visitor/drop_if_exists_test.go +++ b/mdl/visitor/drop_if_exists_test.go @@ -73,3 +73,34 @@ func TestDropDemoUser_IfExistsIsParsed(t *testing.T) { } } } + +// Document DROPs take IF EXISTS too, so a script that removes a layout or a page +// can run a second time (mendixlabs/mxcli#1190). One case per kind: the flag is +// set, and IF EXISTS is not read as part of the name. +func TestDropDocument_IfExistsIsParsed(t *testing.T) { + for _, kind := range []string{ + "entity", "association", "enumeration", "constant", "microflow", "nanoflow", + "page", "layout", "snippet", "menu", "java action", "image collection", + } { + for _, withIf := range []bool{true, false} { + src := "drop " + kind + " M.Doc;" + if withIf { + src = "drop " + kind + " if exists M.Doc;" + } + prog, errs := Build(src) + if len(errs) > 0 { + t.Fatalf("%s: parse: %v", src, errs[0]) + } + if len(prog.Statements) != 1 { + t.Fatalf("%s: got %d statements", src, len(prog.Statements)) + } + skipper, ok := prog.Statements[0].(ast.MissingSkipper) + if !ok { + t.Fatalf("%s: %T does not take IF EXISTS", src, prog.Statements[0]) + } + if skipper.SkipsMissing() != withIf { + t.Errorf("%s: SkipsMissing = %v, want %v", src, skipper.SkipsMissing(), withIf) + } + } + } +} diff --git a/mdl/visitor/visitor_entity.go b/mdl/visitor/visitor_entity.go index 6c152120e..9e6784c6d 100644 --- a/mdl/visitor/visitor_entity.go +++ b/mdl/visitor/visitor_entity.go @@ -825,19 +825,23 @@ func (b *Builder) ExitDropStatement(ctx *parser.DropStatementContext) { if ctx.ENTITY() != nil { b.statements = append(b.statements, &ast.DropEntityStmt{ - Name: buildQualifiedName(names[0]), + Name: buildQualifiedName(names[0]), + DropIfExists: ast.DropIfExists{IfExists: ctx.IfExists() != nil}, }) } else if ctx.ASSOCIATION() != nil { b.statements = append(b.statements, &ast.DropAssociationStmt{ - Name: buildQualifiedName(names[0]), + Name: buildQualifiedName(names[0]), + DropIfExists: ast.DropIfExists{IfExists: ctx.IfExists() != nil}, }) } else if ctx.ENUMERATION() != nil { b.statements = append(b.statements, &ast.DropEnumerationStmt{ - Name: buildQualifiedName(names[0]), + Name: buildQualifiedName(names[0]), + DropIfExists: ast.DropIfExists{IfExists: ctx.IfExists() != nil}, }) } else if ctx.CONSTANT() != nil { b.statements = append(b.statements, &ast.DropConstantStmt{ - Name: buildQualifiedName(names[0]), + Name: buildQualifiedName(names[0]), + DropIfExists: ast.DropIfExists{IfExists: ctx.IfExists() != nil}, }) } else if ctx.MODULE() != nil { name := getQualifiedNameText(names[0]) @@ -846,11 +850,13 @@ func (b *Builder) ExitDropStatement(ctx *parser.DropStatementContext) { }) } else if ctx.MICROFLOW() != nil { b.statements = append(b.statements, &ast.DropMicroflowStmt{ - Name: buildQualifiedName(names[0]), + Name: buildQualifiedName(names[0]), + DropIfExists: ast.DropIfExists{IfExists: ctx.IfExists() != nil}, }) } else if ctx.NANOFLOW() != nil { b.statements = append(b.statements, &ast.DropNanoflowStmt{ - Name: buildQualifiedName(names[0]), + Name: buildQualifiedName(names[0]), + DropIfExists: ast.DropIfExists{IfExists: ctx.IfExists() != nil}, }) } else if ctx.RULE() != nil { b.statements = append(b.statements, &ast.DropRuleStmt{ @@ -858,19 +864,23 @@ func (b *Builder) ExitDropStatement(ctx *parser.DropStatementContext) { }) } else if ctx.PAGE() != nil { b.statements = append(b.statements, &ast.DropPageStmt{ - Name: buildQualifiedName(names[0]), + Name: buildQualifiedName(names[0]), + DropIfExists: ast.DropIfExists{IfExists: ctx.IfExists() != nil}, }) } else if ctx.LAYOUT() != nil { b.statements = append(b.statements, &ast.DropLayoutStmt{ - Name: buildQualifiedName(names[0]), + Name: buildQualifiedName(names[0]), + DropIfExists: ast.DropIfExists{IfExists: ctx.IfExists() != nil}, }) } else if ctx.SNIPPET() != nil { b.statements = append(b.statements, &ast.DropSnippetStmt{ - Name: buildQualifiedName(names[0]), + Name: buildQualifiedName(names[0]), + DropIfExists: ast.DropIfExists{IfExists: ctx.IfExists() != nil}, }) } else if ctx.MENU_KW() != nil { b.statements = append(b.statements, &ast.DropMenuStmt{ - Name: buildQualifiedName(names[0]), + Name: buildQualifiedName(names[0]), + DropIfExists: ast.DropIfExists{IfExists: ctx.IfExists() != nil}, }) } else if ctx.JAVASCRIPT() != nil && ctx.ACTION() != nil { b.statements = append(b.statements, &ast.DropJavaScriptActionStmt{ @@ -878,7 +888,8 @@ func (b *Builder) ExitDropStatement(ctx *parser.DropStatementContext) { }) } else if ctx.JAVA() != nil && ctx.ACTION() != nil { b.statements = append(b.statements, &ast.DropJavaActionStmt{ - Name: buildQualifiedName(names[0]), + Name: buildQualifiedName(names[0]), + DropIfExists: ast.DropIfExists{IfExists: ctx.IfExists() != nil}, }) } else if ctx.ODATA() != nil && ctx.CLIENT() != nil { b.statements = append(b.statements, &ast.DropODataClientStmt{ @@ -898,7 +909,8 @@ func (b *Builder) ExitDropStatement(ctx *parser.DropStatementContext) { }) } else if ctx.IMAGE() != nil && ctx.COLLECTION() != nil { b.statements = append(b.statements, &ast.DropImageCollectionStmt{ - Name: buildQualifiedName(names[0]), + Name: buildQualifiedName(names[0]), + DropIfExists: ast.DropIfExists{IfExists: ctx.IfExists() != nil}, }) } else if ctx.QUEUE() != nil { b.statements = append(b.statements, &ast.DropQueueStmt{