Skip to content

Commit c67c300

Browse files
committed
sql/workload: add triggers to the random schema workload
Added coverage for `CREATE TRIGGER` and `DROP TRIGGER` to the Random Schema Changer Workload. Fixes: cockroachdb#146580 Epic: CRDB-42942 Release note: none
1 parent 798a005 commit c67c300

File tree

4 files changed

+228
-8
lines changed

4 files changed

+228
-8
lines changed

pkg/workload/schemachange/operation_generator.go

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4432,6 +4432,7 @@ FROM
44324432
})
44334433
opStmt.potentialExecErrors.addAll(codesWithConditions{
44344434
{pgcode.InvalidFunctionDefinition, hasFuncRefs},
4435+
{pgcode.FeatureNotSupported, true},
44354436
})
44364437

44374438
return opStmt, nil
@@ -5338,3 +5339,160 @@ func (og *operationGenerator) randUser(ctx context.Context, tx pgx.Tx) (string,
53385339
// There should always be at least one user.
53395340
return "", errors.New("no users found in the database")
53405341
}
5342+
5343+
// createTrigger generates a CREATE TRIGGER statement.
5344+
func (og *operationGenerator) createTrigger(ctx context.Context, tx pgx.Tx) (*opStmt, error) {
5345+
tableName, err := og.randTable(ctx, tx, og.pctExisting(true), "")
5346+
if err != nil {
5347+
return nil, err
5348+
}
5349+
5350+
tableExists, err := og.tableExists(ctx, tx, tableName)
5351+
if err != nil {
5352+
return nil, err
5353+
}
5354+
5355+
// First, ensure the trigger_log table exists
5356+
_, err = tx.Exec(ctx, `
5357+
CREATE TABLE IF NOT EXISTS trigger_log (
5358+
changed_at TIMESTAMP DEFAULT current_timestamp
5359+
)`)
5360+
if err != nil {
5361+
return nil, err
5362+
}
5363+
5364+
// Create TRIGGER statement components
5365+
triggerActionTime := "BEFORE"
5366+
if og.randIntn(2) == 1 {
5367+
triggerActionTime = "AFTER"
5368+
}
5369+
5370+
eventTypes := []string{"INSERT", "UPDATE", "DELETE"}
5371+
numEvents := og.randIntn(3) + 1 // 1-3 events
5372+
events := make([]string, 0, numEvents)
5373+
eventsSet := make(map[string]bool)
5374+
5375+
for i := 0; i < numEvents; i++ {
5376+
eventIndex := og.randIntn(len(eventTypes))
5377+
event := eventTypes[eventIndex]
5378+
if !eventsSet[event] {
5379+
events = append(events, event)
5380+
eventsSet[event] = true
5381+
}
5382+
}
5383+
5384+
// Join events with OR
5385+
eventClause := strings.Join(events, " OR ")
5386+
5387+
triggerName := fmt.Sprintf("trigger_%s", og.newUniqueSeqNumSuffix())
5388+
5389+
// Build the SQL statement
5390+
var sqlStatement strings.Builder
5391+
sqlStatement.WriteString(fmt.Sprintf("CREATE TRIGGER %s %s %s ON %s FOR EACH ROW EXECUTE FUNCTION log_change_timestamp()",
5392+
triggerName, triggerActionTime, eventClause, tableName))
5393+
5394+
og.LogMessage(fmt.Sprintf("createTrigger: %s", sqlStatement.String()))
5395+
5396+
opStmt := makeOpStmt(OpStmtDDL)
5397+
opStmt.sql = sqlStatement.String()
5398+
5399+
opStmt.expectedExecErrors.addAll(codesWithConditions{
5400+
{code: pgcode.FeatureNotSupported, condition: !og.useDeclarativeSchemaChanger},
5401+
{code: pgcode.UndefinedTable, condition: !tableExists},
5402+
})
5403+
5404+
opStmt.potentialExecErrors.addAll(codesWithConditions{
5405+
{code: pgcode.UndefinedFunction, condition: true},
5406+
})
5407+
5408+
return opStmt, nil
5409+
}
5410+
5411+
// dropTrigger generates a DROP TRIGGER statement.
5412+
func (og *operationGenerator) dropTrigger(ctx context.Context, tx pgx.Tx) (*opStmt, error) {
5413+
// Find an existing trigger
5414+
triggerWithInfo, triggerExists, err := findExistingTrigger(ctx, tx, og)
5415+
if err != nil {
5416+
return nil, err
5417+
}
5418+
if !triggerExists {
5419+
5420+
// Build the SQL statement
5421+
var sqlStatement strings.Builder
5422+
sqlStatement.WriteString(`DROP TRIGGER dummy_trigger ON dummy_table`)
5423+
5424+
// Set up expected errors since we know this trigger doesn't exist
5425+
opStmt := makeOpStmt(OpStmtDDL)
5426+
opStmt.sql = sqlStatement.String()
5427+
5428+
opStmt.expectedExecErrors.add(pgcode.UndefinedTable)
5429+
5430+
og.LogMessage(fmt.Sprintf("dropTrigger (non-existent): %s", sqlStatement.String()))
5431+
5432+
return opStmt, nil
5433+
}
5434+
5435+
// Build the SQL statement
5436+
var sqlStatement strings.Builder
5437+
sqlStatement.WriteString(fmt.Sprintf("DROP TRIGGER %s ON %s", triggerWithInfo.triggerName, &triggerWithInfo.table))
5438+
5439+
// Construct DROP TRIGGER statement
5440+
og.LogMessage(fmt.Sprintf("dropTrigger: %s", sqlStatement.String()))
5441+
5442+
opStmt := makeOpStmt(OpStmtDDL)
5443+
opStmt.sql = sqlStatement.String()
5444+
5445+
return opStmt, nil
5446+
}
5447+
5448+
// triggerInfo contains information about a trigger.
5449+
type triggerInfo struct {
5450+
table tree.TableName
5451+
triggerName string
5452+
}
5453+
5454+
// findExistingTrigger returns a triggerInfo struct with the qualified table name and trigger name.
5455+
// It also returns a boolean indicating whether a trigger was found.
5456+
func findExistingTrigger(
5457+
ctx context.Context, tx pgx.Tx, og *operationGenerator,
5458+
) (*triggerInfo, bool, error) {
5459+
var triggerWithInfo triggerInfo
5460+
triggerExists := false
5461+
5462+
// Query to find all triggers in the database using system tables
5463+
triggerQuery := `
5464+
SELECT
5465+
ns.name AS schema_name,
5466+
crdb_internal.pb_to_json('descriptor', d.descriptor) -> 'table' ->> 'name' AS table_name,
5467+
trigger->>'name' AS trigger_name
5468+
FROM system.descriptor AS d
5469+
JOIN system.namespace AS ns
5470+
ON (crdb_internal.pb_to_json('descriptor', d.descriptor) -> 'table' ->> 'unexposedParentSchemaId')::INT = ns.id
5471+
JOIN LATERAL json_array_elements(
5472+
crdb_internal.pb_to_json('descriptor', d.descriptor) -> 'table' -> 'triggers'
5473+
) AS trigger ON TRUE
5474+
WHERE (crdb_internal.pb_to_json('descriptor', d.descriptor) -> 'table' -> 'triggers') IS NOT NULL
5475+
ORDER BY random()
5476+
LIMIT 1
5477+
`
5478+
5479+
var schemaName, tableName, triggerName string
5480+
err := tx.QueryRow(ctx, triggerQuery).Scan(&schemaName, &tableName, &triggerName)
5481+
if err != nil {
5482+
if errors.Is(err, pgx.ErrNoRows) {
5483+
return nil, false, nil
5484+
}
5485+
return nil, false, err
5486+
}
5487+
5488+
triggerWithInfo = triggerInfo{
5489+
table: tree.MakeTableNameFromPrefix(tree.ObjectNamePrefix{
5490+
SchemaName: tree.Name(schemaName),
5491+
ExplicitSchema: true,
5492+
}, tree.Name(tableName)),
5493+
triggerName: triggerName,
5494+
}
5495+
triggerExists = true
5496+
5497+
return &triggerWithInfo, triggerExists, nil
5498+
}

pkg/workload/schemachange/optype.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ const (
127127
createTableAs // CREATE TABLE <table> AS <def>
128128
createView // CREATE VIEW <view> AS <def>
129129
createFunction // CREATE FUNCTION <function> ...
130+
createTrigger // CREATE TRIGGER <trigger> {...} ON <table> EXECUTE FUNCTION <function>()
130131

131132
// COMMENT ON ...
132133

@@ -140,6 +141,7 @@ const (
140141
dropSchema // DROP SCHEMA <schema>
141142
dropSequence // DROP SEQUENCE <sequence>
142143
dropTable // DROP TABLE <table>
144+
dropTrigger // DROP TRIGGER <trigger> ON <table>
143145
dropView // DROP VIEW <view>
144146

145147
// Unimplemented operations. TODO(sql-foundations): Audit and/or implement these operations.
@@ -246,6 +248,7 @@ var opFuncs = []func(*operationGenerator, context.Context, pgx.Tx) (*opStmt, err
246248
createSequence: (*operationGenerator).createSequence,
247249
createTable: (*operationGenerator).createTable,
248250
createTableAs: (*operationGenerator).createTableAs,
251+
createTrigger: (*operationGenerator).createTrigger,
249252
createTypeEnum: (*operationGenerator).createEnum,
250253
createTypeComposite: (*operationGenerator).createCompositeType,
251254
createView: (*operationGenerator).createView,
@@ -255,6 +258,7 @@ var opFuncs = []func(*operationGenerator, context.Context, pgx.Tx) (*opStmt, err
255258
dropSchema: (*operationGenerator).dropSchema,
256259
dropSequence: (*operationGenerator).dropSequence,
257260
dropTable: (*operationGenerator).dropTable,
261+
dropTrigger: (*operationGenerator).dropTrigger,
258262
dropView: (*operationGenerator).dropView,
259263
renameIndex: (*operationGenerator).renameIndex,
260264
renameSequence: (*operationGenerator).renameSequence,
@@ -301,6 +305,7 @@ var opWeights = []int{
301305
createSequence: 1,
302306
createTable: 10,
303307
createTableAs: 1,
308+
createTrigger: 1,
304309
createTypeEnum: 1,
305310
createTypeComposite: 1,
306311
createView: 1,
@@ -310,6 +315,7 @@ var opWeights = []int{
310315
dropSchema: 1,
311316
dropSequence: 1,
312317
dropTable: 1,
318+
dropTrigger: 1,
313319
dropView: 1,
314320
renameIndex: 1,
315321
renameSequence: 1,
@@ -342,11 +348,13 @@ var opDeclarativeVersion = map[opType]clusterversion.Key{
342348
createPolicy: clusterversion.V25_2,
343349
createSchema: clusterversion.MinSupported,
344350
createSequence: clusterversion.MinSupported,
351+
createTrigger: clusterversion.MinSupported,
345352
dropFunction: clusterversion.MinSupported,
346353
dropIndex: clusterversion.MinSupported,
347354
dropPolicy: clusterversion.V25_2,
348355
dropSchema: clusterversion.MinSupported,
349356
dropSequence: clusterversion.MinSupported,
350357
dropTable: clusterversion.MinSupported,
358+
dropTrigger: clusterversion.MinSupported,
351359
dropView: clusterversion.MinSupported,
352360
}

pkg/workload/schemachange/optype_string.go

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

pkg/workload/schemachange/schemachange.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,54 @@ func (s *schemaChange) setClusterSettings(ctx context.Context, url string) (err
359359
return errors.WithStack(err)
360360
}
361361
}
362+
363+
// First, ensure the trigger_log table exists
364+
_, err = conn.Exec(ctx, `
365+
CREATE TABLE IF NOT EXISTS trigger_log (
366+
changed_at TIMESTAMP DEFAULT current_timestamp
367+
)`)
368+
if err != nil {
369+
return errors.WithStack(err)
370+
}
371+
372+
/* // Save the current declarative schema changer setting
373+
var originalDSCValue string
374+
err = conn.QueryRow(ctx, `SHOW use_declarative_schema_changer`).Scan(&originalDSCValue)
375+
if err != nil {
376+
return errors.WithStack(err)
377+
}
378+
379+
// Temporarily disable declarative schema changer for function creation
380+
_, err = conn.Exec(ctx, `SET use_declarative_schema_changer=off`)
381+
if err != nil {
382+
return errors.WithStack(err)
383+
}*/
384+
385+
// Create the function with legacy schema changer
386+
_, err = conn.Exec(ctx, `
387+
CREATE OR REPLACE FUNCTION log_change_timestamp()
388+
RETURNS TRIGGER AS $$
389+
BEGIN
390+
INSERT INTO trigger_log
391+
VALUES (current_timestamp);
392+
RETURN NULL;
393+
END;
394+
$$ LANGUAGE PLpgSQL`)
395+
if err != nil {
396+
/* // Restore original setting even if function creation fails
397+
_, restoreErr := conn.Exec(ctx, fmt.Sprintf(`SET use_declarative_schema_changer=%s`, originalDSCValue))
398+
if restoreErr != nil {
399+
return errors.CombineErrors(err, restoreErr)
400+
}*/
401+
return errors.WithStack(err)
402+
}
403+
404+
/* // Restore the original declarative schema changer setting
405+
_, err = conn.Exec(ctx, fmt.Sprintf(`SET use_declarative_schema_changer=%s`, originalDSCValue))
406+
if err != nil {
407+
return errors.WithStack(err)
408+
}*/
409+
362410
return nil
363411
}
364412

0 commit comments

Comments
 (0)