Skip to content

Commit 9decebf

Browse files
committed
workload/schemachanger: add randomized testing for TRUNCATE
Previously, the randomized schema changer workload had no coverage for TRUNCATE. This patch adds support for testing TRUNCATE in the randomized workload with multiple tables and with / without CASSCADE. Note: This patch also fixes the pgcode generated by TRUNCATE when foreign key refrences exist, since previously we returned an internal error. Postgres generates a FeatureNotSupported pgcode. Fixes: #154045 Release note: None
1 parent f3c24cf commit 9decebf

File tree

6 files changed

+121
-2
lines changed

6 files changed

+121
-2
lines changed

pkg/sql/schemachanger/scbuild/internal/scbuildstmt/truncate.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ package scbuildstmt
88
import (
99
"github.com/cockroachdb/cockroach/pkg/sql/catalog"
1010
"github.com/cockroachdb/cockroach/pkg/sql/catalog/descpb"
11+
"github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgcode"
12+
"github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgerror"
1113
"github.com/cockroachdb/cockroach/pkg/sql/privilege"
1214
"github.com/cockroachdb/cockroach/pkg/sql/schemachanger/scerrors"
1315
"github.com/cockroachdb/cockroach/pkg/sql/schemachanger/scpb"
@@ -51,7 +53,13 @@ func Truncate(b BuildCtx, stmt *tree.Truncate) {
5153
if stmt.DropBehavior != tree.DropCascade {
5254
name := b.QueryByID(tableID).FilterNamespace().MustGetOneElement()
5355
refName := refElts.FilterNamespace().MustGetZeroOrOneElement()
54-
panic(errors.Errorf("%q is %s table %q", name.Name, "referenced by foreign key from", refName.Name))
56+
// The error code returned here matches PGSQL, even though the CASCADE
57+
// operation is supported there with TRUNCATE as well.
58+
panic(errors.WithHint(pgerror.Newf(pgcode.FeatureNotSupported,
59+
"%q is %s table %q", name.Name,
60+
"referenced by foreign key from",
61+
refName.Name),
62+
"truncate dependent tables at the same time or specify the CASCADE option"))
5563
}
5664
if !tablesToTruncate.Contains(referencingTableID) {
5765
tablesToTruncate.Add(referencingTableID)

pkg/sql/truncate.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import (
1818
"github.com/cockroachdb/cockroach/pkg/sql/catalog/catalogkeys"
1919
"github.com/cockroachdb/cockroach/pkg/sql/catalog/descpb"
2020
"github.com/cockroachdb/cockroach/pkg/sql/catalog/tabledesc"
21+
"github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgcode"
22+
"github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgerror"
2123
"github.com/cockroachdb/cockroach/pkg/sql/privilege"
2224
"github.com/cockroachdb/cockroach/pkg/sql/schemachanger/scerrors"
2325
"github.com/cockroachdb/cockroach/pkg/sql/sem/tree"
@@ -89,7 +91,10 @@ func (t *truncateNode) startExec(params runParams) error {
8991
}
9092

9193
if n.DropBehavior != tree.DropCascade {
92-
return errors.Errorf("%q is %s table %q", tableDesc.Name, msg, other.Name)
94+
// The error code returned here matches PGSQL, even though the CASCADE
95+
// operation is supported there with TRUNCATE as well.
96+
return errors.WithHintf(pgerror.Newf(pgcode.FeatureNotSupported, "%q is %s table %q", tableDesc.Name, msg, other.Name),
97+
"truncate dependent tables at the same time or specify the CASCADE option")
9398
}
9499
if err := p.CheckPrivilege(ctx, other, privilege.DROP); err != nil {
95100
return err

pkg/workload/schemachange/error_screening.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1794,3 +1794,38 @@ func (og *operationGenerator) tableHasForeignKeyMutation(
17941794
AND (m->'constraint'->>'foreign_key') IS NOT NULL
17951795
);`, tableName)
17961796
}
1797+
1798+
// getTableForeignKeyReferences returns a list of tables that reference
1799+
// the specified table via foreign key references.
1800+
func (og *operationGenerator) getTableForeignKeyReferences(
1801+
ctx context.Context, tx pgx.Tx, tableName *tree.TableName,
1802+
) ([]tree.TableName, error) {
1803+
rows, err := tx.Query(ctx,
1804+
`WITH fk_refs AS (
1805+
SELECT conrelid FROM pg_constraint WHERE
1806+
confrelid = $1::REGCLASS AND
1807+
conrelid <> $1::REGCLASS
1808+
)
1809+
SELECT
1810+
n.nspname as schema_name, c.relname AS object_name
1811+
FROM fk_refs AS f
1812+
JOIN pg_class AS c ON c.oid = f.conrelid
1813+
JOIN pg_namespace AS n ON c.relnamespace = n.oid
1814+
`,
1815+
tableName.String())
1816+
if err != nil {
1817+
return nil, err
1818+
}
1819+
defer rows.Close()
1820+
var result []tree.TableName
1821+
for rows.Next() {
1822+
var table, schema string
1823+
err = rows.Scan(&schema, &table)
1824+
if err != nil {
1825+
return nil, err
1826+
}
1827+
name := tree.MakeTableNameWithSchema(tableName.CatalogName, tree.Name(schema), tree.Name(table))
1828+
result = append(result, name)
1829+
}
1830+
return result, rows.Err()
1831+
}

pkg/workload/schemachange/operation_generator.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5580,3 +5580,67 @@ func (og *operationGenerator) findExistingTrigger(
55805580

55815581
return &triggerWithInfo, nil
55825582
}
5583+
5584+
// truncateTable generates a TRUNCATE TABLE statement.
5585+
func (og *operationGenerator) truncateTable(ctx context.Context, tx pgx.Tx) (*opStmt, error) {
5586+
tbls, err := Collect(ctx, og, tx, pgx.RowTo[string],
5587+
`SELECT quote_ident(schema_name) || '.' || quote_ident(table_name) FROM [SHOW TABLES] WHERE type = 'table'`)
5588+
if err != nil {
5589+
return nil, err
5590+
}
5591+
5592+
const MaxTruncateTables = 8
5593+
stmt, expectedCode, err := Generate[*tree.Truncate](og.params.rng, og.produceError(), []GenerationCase{
5594+
{pgcode.SuccessfulCompletion, `TRUNCATE TABLE {MultipleTableNames}`},
5595+
{pgcode.SuccessfulCompletion, `TRUNCATE TABLE {MultipleTableNames} CASCADE`},
5596+
{pgcode.UndefinedTable, `TRUNCATE TABLE {TableNameDoesNotExist}`},
5597+
{pgcode.UndefinedTable, `TRUNCATE TABLE {TableNameDoesNotExist} CASCADE`},
5598+
}, template.FuncMap{
5599+
"MultipleTableNames": func() (string, error) {
5600+
selectedTables, err := PickBetween(og.params.rng, 1, MaxTruncateTables, tbls)
5601+
if err != nil {
5602+
return "", err
5603+
}
5604+
return strings.Join(selectedTables, ","), nil
5605+
},
5606+
"TableNameDoesNotExist": func() (string, error) {
5607+
return "TableThatDoesNotExist", nil
5608+
},
5609+
})
5610+
if err != nil {
5611+
return nil, err
5612+
}
5613+
op := newOpStmt(stmt, codesWithConditions{
5614+
{expectedCode, true},
5615+
})
5616+
5617+
// If the the TRUNCATE is not cascaded, then the operation can fail
5618+
// if foreign key references exist.
5619+
if expectedCode == pgcode.SuccessfulCompletion &&
5620+
stmt.DropBehavior != tree.DropCascade {
5621+
tableSet := map[string]struct{}{}
5622+
fkSet := map[string]struct{}{}
5623+
// Gather the set of tables handled by this statement, and
5624+
// any foreign keys that reference these tables.
5625+
for _, table := range stmt.Tables {
5626+
tableSet[table.FQString()] = struct{}{}
5627+
fkReferenceTables, err := og.getTableForeignKeyReferences(ctx, tx, &table)
5628+
if err != nil {
5629+
return nil, err
5630+
}
5631+
for _, fk := range fkReferenceTables {
5632+
fkSet[fk.FQString()] = struct{}{}
5633+
}
5634+
}
5635+
// Check if any of the foreign keys that reference these
5636+
// tables are not truncated. If they are, then the TRUNCATE
5637+
// will fail with an error.
5638+
for fk := range fkSet {
5639+
if _, hasTable := tableSet[fk]; !hasTable {
5640+
op.expectedExecErrors.add(pgcode.FeatureNotSupported)
5641+
break
5642+
}
5643+
}
5644+
}
5645+
return op, nil
5646+
}

pkg/workload/schemachange/optype.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ const (
143143
dropTable // DROP TABLE <table>
144144
dropTrigger // DROP TRIGGER <trigger> ON <table>
145145
dropView // DROP VIEW <view>
146+
truncateTable
146147

147148
// Unimplemented operations. TODO(sql-foundations): Audit and/or implement these operations.
148149
// alterDatabaseOwner
@@ -264,6 +265,7 @@ var opFuncs = []func(*operationGenerator, context.Context, pgx.Tx) (*opStmt, err
264265
renameSequence: (*operationGenerator).renameSequence,
265266
renameTable: (*operationGenerator).renameTable,
266267
renameView: (*operationGenerator).renameView,
268+
truncateTable: (*operationGenerator).truncateTable,
267269
}
268270

269271
var opWeights = []int{
@@ -321,6 +323,7 @@ var opWeights = []int{
321323
renameSequence: 1,
322324
renameTable: 1,
323325
renameView: 1,
326+
truncateTable: 1,
324327
}
325328

326329
// This workload will maintain its own list of minimal supported versions for
@@ -358,4 +361,5 @@ var opDeclarativeVersion = map[opType]clusterversion.Key{
358361
dropTable: clusterversion.MinSupported,
359362
dropTrigger: clusterversion.MinSupported,
360363
dropView: clusterversion.MinSupported,
364+
truncateTable: clusterversion.V25_4,
361365
}

pkg/workload/schemachange/optype_string.go

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)