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{