Skip to content

Commit 01c9276

Browse files
craig[bot]rafiss
andcommitted
Merge #147173
147173: sql: clean up comments in legacy DROP TYPE r=rafiss a=rafiss The cleanup logic was not removing comments on types. This was caught by the new validation added in #146213. This patch adds a logic test that reveals the bug, and also adds validation to TestWorkload. This validation is already in the schemachange/random-load roachtest. Doing it here too can help catch bugs earlier. fixes #146516 fixes #146913 fixes #146793 Release note: None Co-authored-by: Rafi Shamim <[email protected]>
2 parents 0335358 + e85c794 commit 01c9276

File tree

7 files changed

+192
-48
lines changed

7 files changed

+192
-48
lines changed

pkg/ccl/testccl/workload/schemachange/schema_change_external_test.go

Lines changed: 58 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ import (
2323
"github.com/cockroachdb/cockroach/pkg/testutils/sqlutils"
2424
"github.com/cockroachdb/cockroach/pkg/util/leaktest"
2525
"github.com/cockroachdb/cockroach/pkg/util/log"
26+
"github.com/cockroachdb/cockroach/pkg/util/randutil"
2627
"github.com/cockroachdb/cockroach/pkg/workload"
2728
"github.com/cockroachdb/cockroach/pkg/workload/histogram"
28-
_ "github.com/cockroachdb/cockroach/pkg/workload/schemachange"
29+
"github.com/cockroachdb/cockroach/pkg/workload/schemachange"
2930
"github.com/stretchr/testify/require"
3031
"golang.org/x/sync/errgroup"
3132
)
@@ -36,6 +37,7 @@ func TestWorkload(t *testing.T) {
3637
skip.UnderDeadlock(t, "test connections can be too slow under expensive configs")
3738
skip.UnderRace(t, "test connections can be too slow under expensive configs")
3839

40+
rng, _ := randutil.NewTestRand()
3941
scope := log.Scope(t)
4042
defer scope.Close(t)
4143
dir := scope.GetDirectory()
@@ -56,7 +58,8 @@ func TestWorkload(t *testing.T) {
5658
workload.Opser
5759
workload.Flagser
5860
})
59-
tdb := sqlutils.MakeSQLRunner(tc.ServerConn(0))
61+
db := tc.ServerConn(0)
62+
tdb := sqlutils.MakeSQLRunner(db)
6063
reg := histogram.NewRegistry(20*time.Second, m.Name)
6164
tdb.Exec(t, "CREATE USER testuser")
6265
tdb.Exec(t, "CREATE DATABASE schemachange")
@@ -70,31 +73,61 @@ func TestWorkload(t *testing.T) {
7073
require.NoError(t, os.WriteFile(fmt.Sprintf("%s/%s.rows", dir, name), []byte(sqlutils.MatrixToStr(mat)), 0666))
7174
}
7275

73-
// Grab a backup, dump the namespace and descriptor tables upon failure.
76+
findInvalidObjects := func() {
77+
t.Helper()
78+
invalidObjects, err := schemachange.ValidateInvalidObjects(ctx, db)
79+
if err != nil {
80+
t.Fatal(err)
81+
}
82+
for _, obj := range invalidObjects {
83+
t.Logf(
84+
"invalid object found: id: %d, database_name: %s, schema_name: %s, obj_name: %s, error: %v",
85+
obj.ID, obj.DatabaseName, obj.SchemaName, obj.ObjName, obj.Error,
86+
)
87+
}
88+
if len(invalidObjects) > 0 {
89+
t.Errorf("found %d invalid objects", len(invalidObjects))
90+
}
91+
}
92+
7493
defer func() {
75-
if !t.Failed() {
76-
return
94+
// Run validation before dropping the database.
95+
findInvalidObjects()
96+
97+
// Only take a backup if the test failed.
98+
if t.Failed() {
99+
// Dump namespace and descriptor in their raw format. This is useful for
100+
// processing results with some degree of scripting.
101+
dumpRows("namespace", tdb.Query(t, `SELECT * FROM system.namespace`))
102+
dumpRows("descriptor", tdb.Query(t, "SELECT id, encode(descriptor, 'hex') FROM system.descriptor"))
103+
// Dump out a more human readable version of the above as well to allow for
104+
// easy debugging by hand.
105+
// NB: A LEFT JOIN is used here because not all descriptors (looking at you
106+
// functions) have namespace entries.
107+
dumpRows("ns-desc-json", tdb.Query(t, `
108+
SELECT
109+
"parentID",
110+
"parentSchemaID",
111+
descriptor.id,
112+
name,
113+
crdb_internal.pb_to_json('cockroach.sql.sqlbase.Descriptor', descriptor)
114+
FROM system.descriptor
115+
LEFT JOIN system.namespace ON namespace.id = descriptor.id
116+
`))
117+
tdb.Exec(t, "BACKUP DATABASE schemachange INTO 'nodelocal://1/backup'")
118+
t.Logf("backup, tracing data, and system table dumps in %s", dir)
119+
}
120+
121+
// Drop the database and run validation again. Test DROP DATABASE behavior
122+
// with legacy schema changer 50% of the time.
123+
schemaChangerSetting := "on"
124+
if rng.Float32() < 0.5 {
125+
schemaChangerSetting = "off"
77126
}
78-
// Dump namespace and descriptor in their raw format. This is useful for
79-
// processing results with some degree of scripting.
80-
dumpRows("namespace", tdb.Query(t, `SELECT * FROM system.namespace`))
81-
dumpRows("descriptor", tdb.Query(t, "SELECT id, encode(descriptor, 'hex') FROM system.descriptor"))
82-
// Dump out a more human readable version of the above as well to allow for
83-
// easy debugging by hand.
84-
// NB: A LEFT JOIN is used here because not all descriptors (looking at you
85-
// functions) have namespace entries.
86-
dumpRows("ns-desc-json", tdb.Query(t, `
87-
SELECT
88-
"parentID",
89-
"parentSchemaID",
90-
descriptor.id,
91-
name,
92-
crdb_internal.pb_to_json('cockroach.sql.sqlbase.Descriptor', descriptor)
93-
FROM system.descriptor
94-
LEFT JOIN system.namespace ON namespace.id = descriptor.id
95-
`))
96-
tdb.Exec(t, "BACKUP DATABASE schemachange INTO 'nodelocal://1/backup'")
97-
t.Logf("backup, tracing data, and system table dumps in %s", dir)
127+
t.Logf("running DROP with use_declarative_schema_changer = %s", schemaChangerSetting)
128+
tdb.Exec(t, "SET use_declarative_schema_changer = $1", schemaChangerSetting)
129+
tdb.Exec(t, "DROP DATABASE schemachange CASCADE")
130+
findInvalidObjects()
98131
}()
99132

100133
pgURL, cleanup := pgurlutils.PGUrl(t, tc.Server(0).AdvSQLAddr(), t.Name(), url.User("testuser"))

pkg/cmd/roachtest/tests/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,7 @@ go_library(
304304
"//pkg/workload/debug",
305305
"//pkg/workload/histogram",
306306
"//pkg/workload/histogram/exporter",
307+
"//pkg/workload/schemachange",
307308
"//pkg/workload/tpcc",
308309
"//pkg/workload/tpcds",
309310
"//pkg/workload/tpch",

pkg/cmd/roachtest/tests/schemachange_random_load.go

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"context"
1010
gosql "database/sql"
1111
"fmt"
12+
"math/rand/v2"
1213
"path/filepath"
1314

1415
"github.com/cockroachdb/cockroach/pkg/cmd/roachtest/cluster"
@@ -18,6 +19,7 @@ import (
1819
"github.com/cockroachdb/cockroach/pkg/cmd/roachtest/spec"
1920
"github.com/cockroachdb/cockroach/pkg/cmd/roachtest/test"
2021
"github.com/cockroachdb/cockroach/pkg/roachprod/install"
22+
"github.com/cockroachdb/cockroach/pkg/workload/schemachange"
2123
)
2224

2325
const (
@@ -57,33 +59,18 @@ func runSchemaChangeRandomLoad(
5759
ctx context.Context, t test.Test, c cluster.Cluster, maxOps, concurrency int,
5860
) {
5961
validate := func(db *gosql.DB) {
60-
var (
61-
id int
62-
databaseName string
63-
schemaName string
64-
objName string
65-
objError string
66-
)
67-
numInvalidObjects := 0
68-
rows, err := db.QueryContext(ctx, `SELECT id, database_name, schema_name, obj_name, error FROM crdb_internal.invalid_objects`)
62+
invalidObjects, err := schemachange.ValidateInvalidObjects(ctx, db)
6963
if err != nil {
7064
t.Fatal(err)
7165
}
72-
for rows.Next() {
73-
numInvalidObjects++
74-
if err := rows.Scan(&id, &databaseName, &schemaName, &objName, &objError); err != nil {
75-
t.Fatal(err)
76-
}
66+
for _, obj := range invalidObjects {
7767
t.L().Errorf(
78-
"invalid object found: id: %d, database_name: %s, schema_name: %s, obj_name: %s, error: %s",
79-
id, databaseName, schemaName, objName, objError,
68+
"invalid object found: id: %d, database_name: %s, schema_name: %s, obj_name: %s, error: %v",
69+
obj.ID, obj.DatabaseName, obj.SchemaName, obj.ObjName, obj.Error,
8070
)
8171
}
82-
if err := rows.Err(); err != nil {
83-
t.Fatal(err)
84-
}
85-
if numInvalidObjects > 0 {
86-
t.Fatalf("found %d invalid objects", numInvalidObjects)
72+
if len(invalidObjects) > 0 {
73+
t.Fatalf("found %d invalid objects", len(invalidObjects))
8774
}
8875
}
8976
loadNode := c.Node(1)
@@ -142,8 +129,16 @@ func runSchemaChangeRandomLoad(
142129

143130
t.Status("performing validation after workload")
144131
validate(db)
145-
t.Status("dropping database")
146-
_, err = db.ExecContext(ctx, `USE defaultdb; DROP DATABASE schemachange CASCADE;`)
132+
schemaChangerSetting := "on"
133+
if rand.Float32() < 0.5 {
134+
schemaChangerSetting = "off"
135+
}
136+
t.Status(fmt.Sprintf("dropping database with use_declarative_schema_changer = %s", schemaChangerSetting))
137+
_, err = db.ExecContext(ctx, "SET use_declarative_schema_changer = $1", schemaChangerSetting)
138+
if err != nil {
139+
t.Fatal(err)
140+
}
141+
_, err = db.ExecContext(ctx, "DROP DATABASE schemachange CASCADE")
147142
if err != nil {
148143
t.Fatal(err)
149144
}

pkg/sql/drop_type.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"fmt"
1111

1212
"github.com/cockroachdb/cockroach/pkg/sql/catalog"
13+
"github.com/cockroachdb/cockroach/pkg/sql/catalog/catalogkeys"
1314
"github.com/cockroachdb/cockroach/pkg/sql/catalog/descpb"
1415
"github.com/cockroachdb/cockroach/pkg/sql/catalog/tabledesc"
1516
"github.com/cockroachdb/cockroach/pkg/sql/catalog/typedesc"
@@ -291,6 +292,11 @@ func (p *planner) dropTypeImpl(
291292
return err
292293
}
293294

295+
// Delete any comments associated with this type.
296+
if err := p.deleteComment(ctx, typeDesc.ID, 0, catalogkeys.TypeCommentType); err != nil {
297+
return err
298+
}
299+
294300
// For this type descriptor, delete queued schema change jobs from the job
295301
// cache. If the job had already been scheduled, we would also need to mark
296302
// these jobs as successful, but that should never happen for types.

pkg/sql/logictest/testdata/logic_test/comment_on

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,3 +578,60 @@ SELECT * FROM crdb_internal.invalid_objects ORDER BY id;
578578
----
579579

580580
subtest end
581+
582+
# Test for issue #146516: Ensure type comments are cleaned up when dropping types
583+
# This test verifies that COMMENT ON TYPE followed by DROP DATABASE CASCADE
584+
# properly cleans up orphaned comments to avoid "invalid objects" errors.
585+
subtest type_comment_cleanup_on_drop_database_cascade
586+
587+
statement ok
588+
CREATE DATABASE test_db
589+
590+
statement ok
591+
USE test_db
592+
593+
statement ok
594+
CREATE TYPE roach_type AS ENUM ('option1', 'option2')
595+
596+
# Skip for legacy schema changer as COMMENT ON TYPE is not supported
597+
skipif config local-legacy-schema-changer
598+
statement ok
599+
COMMENT ON TYPE roach_type IS 'This is a test comment on a type'
600+
601+
skipif config local-legacy-schema-changer
602+
query TTTT colnames
603+
SHOW TYPES WITH COMMENT
604+
----
605+
schema name owner comment
606+
public roach_type root This is a test comment on a type
607+
608+
# Now drop the database with CASCADE, which should clean up type comments
609+
statement ok
610+
USE defaultdb
611+
612+
let $schema_changer_state
613+
SHOW use_declarative_schema_changer
614+
615+
statement ok
616+
SET use_declarative_schema_changer = 'off'
617+
618+
statement ok
619+
DROP DATABASE test_db CASCADE
620+
621+
# Restore the schema changer state back.
622+
statement ok
623+
SET use_declarative_schema_changer = $schema_changer_state
624+
625+
# Check that no invalid objects exist - this should be empty
626+
# The issue was that type comments were not being cleaned up,
627+
# leaving orphaned entries in system.comments
628+
query ITTTT
629+
SELECT id, database_name, schema_name, obj_name, error FROM "".crdb_internal.invalid_objects
630+
----
631+
632+
# Verify the database is actually gone
633+
query T
634+
SELECT database_name FROM [SHOW DATABASES] WHERE database_name = 'test_db'
635+
----
636+
637+
subtest end

pkg/workload/schemachange/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ go_library(
1414
"schemachange.go",
1515
"tracing.go",
1616
"type_resolver.go",
17+
"validation.go",
1718
"watch_dog.go",
1819
"workload_result.go",
1920
":gen-optype-stringer", # keep
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright 2025 The Cockroach Authors.
2+
//
3+
// Use of this software is governed by the CockroachDB Software License
4+
// included in the /LICENSE file.
5+
6+
package schemachange
7+
8+
import (
9+
"context"
10+
gosql "database/sql"
11+
12+
"github.com/cockroachdb/errors"
13+
)
14+
15+
// InvalidObject represents an invalid database object found during validation.
16+
type InvalidObject struct {
17+
ID int
18+
DatabaseName string
19+
SchemaName string
20+
ObjName string
21+
Error string
22+
}
23+
24+
// ValidateInvalidObjects checks for invalid objects in the database by querying
25+
// crdb_internal.invalid_objects. It returns a slice of InvalidObject structs
26+
// representing any invalid objects found, or an error if the query fails.
27+
//
28+
// This function is useful for validating that schema change operations haven't
29+
// left the database in an inconsistent state with orphaned or invalid objects.
30+
func ValidateInvalidObjects(ctx context.Context, db *gosql.DB) ([]InvalidObject, error) {
31+
query := `SELECT id, database_name, schema_name, obj_name, error FROM crdb_internal.invalid_objects`
32+
rows, err := db.QueryContext(ctx, query)
33+
if err != nil {
34+
return nil, errors.Wrapf(err, "failed to query invalid objects")
35+
}
36+
37+
var invalidObjects []InvalidObject
38+
for rows.Next() {
39+
var obj InvalidObject
40+
if err := rows.Scan(&obj.ID, &obj.DatabaseName, &obj.SchemaName, &obj.ObjName, &obj.Error); err != nil {
41+
return nil, errors.Wrapf(err, "failed to scan invalid object row")
42+
}
43+
invalidObjects = append(invalidObjects, obj)
44+
}
45+
46+
if err := rows.Err(); err != nil {
47+
return nil, errors.Wrapf(err, "error iterating invalid objects")
48+
}
49+
50+
return invalidObjects, nil
51+
}

0 commit comments

Comments
 (0)