fix(coderd/database): ensure task name uniqueness (#20236)

This change ensures task names are unique per user the same way we do
for workspaces. This ensures we don't create tasks that are impossible
to start due to another task being named the same creating a workspace
name conflict.

Updates coder/internal#948
Supersedes coder/coder#20212
This commit is contained in:
Mathias Fredriksson
2025-10-13 12:42:38 +03:00
committed by GitHub
parent 952c69f412
commit 5dc57da6b4
5 changed files with 90 additions and 0 deletions
+4
View File
@@ -3345,6 +3345,10 @@ CREATE INDEX tasks_organization_id_idx ON tasks USING btree (organization_id);
CREATE INDEX tasks_owner_id_idx ON tasks USING btree (owner_id);
CREATE UNIQUE INDEX tasks_owner_id_name_unique_idx ON tasks USING btree (owner_id, lower(name)) WHERE (deleted_at IS NULL);
COMMENT ON INDEX tasks_owner_id_name_unique_idx IS 'Index to ensure uniqueness for task owner/name';
CREATE INDEX tasks_workspace_id_idx ON tasks USING btree (workspace_id);
CREATE INDEX template_usage_stats_start_time_idx ON template_usage_stats USING btree (start_time DESC);
@@ -0,0 +1 @@
DROP INDEX IF EXISTS tasks_owner_id_name_unique_idx;
@@ -0,0 +1,2 @@
CREATE UNIQUE INDEX IF NOT EXISTS tasks_owner_id_name_unique_idx ON tasks (owner_id, LOWER(name)) WHERE deleted_at IS NULL;
COMMENT ON INDEX tasks_owner_id_name_unique_idx IS 'Index to ensure uniqueness for task owner/name';
+82
View File
@@ -7105,6 +7105,88 @@ func TestGetTaskByWorkspaceID(t *testing.T) {
}
}
func TestTaskNameUniqueness(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
org := dbgen.Organization(t, db, database.Organization{})
user1 := dbgen.User(t, db, database.User{})
user2 := dbgen.User(t, db, database.User{})
template := dbgen.Template(t, db, database.Template{
OrganizationID: org.ID,
CreatedBy: user1.ID,
})
tv := dbgen.TemplateVersion(t, db, database.TemplateVersion{
TemplateID: uuid.NullUUID{UUID: template.ID, Valid: true},
OrganizationID: org.ID,
CreatedBy: user1.ID,
})
taskName := "my-task"
// Create initial task for user1.
task1 := dbgen.Task(t, db, database.TaskTable{
OrganizationID: org.ID,
OwnerID: user1.ID,
Name: taskName,
TemplateVersionID: tv.ID,
Prompt: "Test prompt",
})
require.NotEqual(t, uuid.Nil, task1.ID)
tests := []struct {
name string
ownerID uuid.UUID
taskName string
wantErr bool
}{
{
name: "duplicate task name same user",
ownerID: user1.ID,
taskName: taskName,
wantErr: true,
},
{
name: "duplicate task name different case same user",
ownerID: user1.ID,
taskName: "MY-TASK",
wantErr: true,
},
{
name: "same task name different user",
ownerID: user2.ID,
taskName: taskName,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
task, err := db.InsertTask(ctx, database.InsertTaskParams{
OrganizationID: org.ID,
OwnerID: tt.ownerID,
Name: tt.taskName,
TemplateVersionID: tv.ID,
TemplateParameters: json.RawMessage("{}"),
Prompt: "Test prompt",
CreatedAt: dbtime.Now(),
})
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
require.NotEqual(t, uuid.Nil, task.ID)
require.NotEqual(t, task1.ID, task.ID)
}
})
}
}
func TestUsageEventsTrigger(t *testing.T) {
t.Parallel()
+1
View File
@@ -121,6 +121,7 @@ const (
UniqueNotificationMessagesDedupeHashIndex UniqueConstraint = "notification_messages_dedupe_hash_idx" // CREATE UNIQUE INDEX notification_messages_dedupe_hash_idx ON notification_messages USING btree (dedupe_hash);
UniqueOrganizationsSingleDefaultOrg UniqueConstraint = "organizations_single_default_org" // CREATE UNIQUE INDEX organizations_single_default_org ON organizations USING btree (is_default) WHERE (is_default = true);
UniqueProvisionerKeysOrganizationIDNameIndex UniqueConstraint = "provisioner_keys_organization_id_name_idx" // CREATE UNIQUE INDEX provisioner_keys_organization_id_name_idx ON provisioner_keys USING btree (organization_id, lower((name)::text));
UniqueTasksOwnerIDNameUniqueIndex UniqueConstraint = "tasks_owner_id_name_unique_idx" // CREATE UNIQUE INDEX tasks_owner_id_name_unique_idx ON tasks USING btree (owner_id, lower(name)) WHERE (deleted_at IS NULL);
UniqueTemplateUsageStatsStartTimeTemplateIDUserIDIndex UniqueConstraint = "template_usage_stats_start_time_template_id_user_id_idx" // CREATE UNIQUE INDEX template_usage_stats_start_time_template_id_user_id_idx ON template_usage_stats USING btree (start_time, template_id, user_id);
UniqueTemplatesOrganizationIDNameIndex UniqueConstraint = "templates_organization_id_name_idx" // CREATE UNIQUE INDEX templates_organization_id_name_idx ON templates USING btree (organization_id, lower((name)::text)) WHERE (deleted = false);
UniqueUserLinksLinkedIDLoginTypeIndex UniqueConstraint = "user_links_linked_id_login_type_idx" // CREATE UNIQUE INDEX user_links_linked_id_login_type_idx ON user_links USING btree (linked_id, login_type) WHERE (linked_id <> ''::text);