bytebase / bytebase/omni

pg/catalog: dropping a commented object emits COMMENT ... IS NULL after the DROP, so the migration fails to apply

Open
#408 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Go
Stars
15
Forks
7
Avg merge
13h 2m
Merged PRs (30d)
23

Description

Summary

When an object carrying a comment is dropped, GenerateMigration emits a COMMENT ON … IS NULL for it in PhaseMain, after the object's own DROP in PhasePre. PostgreSQL has no COMMENT … IF EXISTS, so the statement errors and the whole migration fails on apply.

This is not specific to one object kind — tables, views and procedures all reproduce.

Reproduction

Applied to postgres:16-alpine via the startPGContainer harness, seeding from, generating the plan, then running plan.SQL():

drop table with comment
  [0] phase=0 DROP TABLE "public"."t" CASCADE
  [1] phase=1 COMMENT ON TABLE "public"."t" IS NULL
  >>> APPLY FAILED: ERROR: relation "public.t" does not exist (SQLSTATE 42P01)

drop procedure with comment
  [0] phase=0 DROP PROCEDURE "public"."purge_rows"()
  [1] phase=1 COMMENT ON PROCEDURE public.purge_rows() IS NULL
  >>> APPLY FAILED: ERROR: procedure public.purge_rows() does not exist (SQLSTATE 42883)

drop view with comment
  [0] phase=0 DROP VIEW "public"."v"
  [1] phase=1 COMMENT ON VIEW "public"."v" IS NULL
  >>> APPLY FAILED: ERROR: relation "public.v" does not exist (SQLSTATE 42P01)

Fixtures:

-- table
CREATE TABLE t (id int); COMMENT ON TABLE t IS 'a table';           -- to: empty
-- procedure
CREATE PROCEDURE purge_rows() LANGUAGE plpgsql AS $$ BEGIN NULL; END; $$;
COMMENT ON PROCEDURE purge_rows() IS 'Purges old rows';             -- to: empty
-- view
CREATE TABLE b (id int); CREATE VIEW v AS SELECT id FROM b;
COMMENT ON VIEW v IS 'a view';                                      -- to: CREATE TABLE b (id int);

Cause

diffComments (pg/catalog/diff_comment.go) compares name-keyed comment maps. When an object is dropped its comment leaves toMap, which yields a DiffDrop entry indistinguishable from "the comment was removed but the object stayed". generateCommentDDL (pg/catalog/migration_comment.go) then renders every DiffDrop entry as COMMENT ON … IS NULL at PhaseMain:

case DiffDrop:
    sql := formatCommentSQL(from, entry.ObjType, entry.ObjDescription, entry.SubID, "")
    if sql != "" {
        ops = append(ops, MigrationOp{Type: OpComment, …, Phase: PhaseMain, …})
    }

A comment does not outlive its object, so the op is redundant as well as invalid: dropping the object already removes the comment.

Suggested fix

Suppress the DiffDrop comment op when the object it targets is absent from the to catalog. commentObjectTarget formats rather than resolves, so this needs a real existence check per ObjType — conservatively keeping the op for any type that cannot be resolved, so no currently-valid output changes.

Why it has not been caught

migration_roundtrip_test.go applies the plan with LoadSQL(from + migration) — omni's own executor accepts COMMENT ON for a missing object, so the round trip passes. The container tests apply against a real Postgres, but none of them drops a commented object.

Worth adding a container case that applies a plan dropping a commented object, since that is the check that would have failed.

Found via

https://github.com/bytebase/omni/pull/407, where a Codex review flagged the procedure case. The reproduction above extends it to tables and views.

🤖 Generated with Claude Code

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with pg/catalog/diff_comment.go and pg/catalog/migration_comment.go, tracing diffComments, commentObjectTarget, and generateCommentDDL. Review migration_roundtrip_test.go and the startPGContainer harness, then add or run a real-PostgreSQL case that drops commented tables, views, and procedures. Done means applying the generated plan succeeds without COMMENT statements targeting dropped objects.

Written by the indexing model from the issue text.

Assessment

Tech stack
go, postgresql
Domain
databases
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.