{"record":{"id":"4dd9f1ceac751b55","repo":"actualbudget/actual","slug":"a-existinggroup-hidden-hidden-exist","errorCode":null,"errorMessage":"A ${existingGroup.hidden ? 'hidden ' : ''}'${existingGroup.name}' category group already exists.","messagePattern":"A (.+?)'(.+?)' category group already exists\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/loot-core/src/server/db/index.ts","lineNumber":370,"sourceCode":"\n  return groups.map(group => ({\n    ...group,\n    categories: categories.filter(c => c.cat_group === group.id),\n  }));\n}\n\nexport async function insertCategoryGroup(\n  group: WithRequired<Partial<DbCategoryGroup>, 'name'>,\n): Promise<DbCategoryGroup['id']> {\n  // Don't allow duplicate group\n  const existingGroup = await first<\n    Pick<DbCategoryGroup, 'id' | 'name' | 'hidden'>\n  >(\n    `SELECT id, name, hidden FROM category_groups WHERE UPPER(name) = ? and tombstone = 0 LIMIT 1`,\n    [group.name.toUpperCase()],\n  );\n  if (existingGroup) {\n    throw new Error(\n      `A ${\n        existingGroup.hidden ? 'hidden ' : ''\n      }'${existingGroup.name}' category group already exists.`,\n    );\n  }\n\n  const lastGroup = await first<Pick<DbCategoryGroup, 'sort_order'>>(`\n    SELECT sort_order FROM category_groups WHERE tombstone = 0 ORDER BY sort_order DESC, id DESC LIMIT 1\n  `);\n  const sort_order = (lastGroup ? lastGroup.sort_order : 0) + SORT_INCREMENT;\n\n  group = {\n    ...categoryGroupModel.validate(group),\n    sort_order,\n  };\n  const id: DbCategoryGroup['id'] = await insertWithUUID(\n    'category_groups',\n    group,","sourceCodeStart":352,"sourceCodeEnd":388,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/loot-core/src/server/db/index.ts#L352-L388","documentation":"insertCategoryGroup performs a case-insensitive uniqueness check (UPPER(name) = ? AND tombstone = 0) before inserting. If any live category group already has the same name (case-insensitive), it throws a message that also indicates whether the existing group is hidden.","triggerScenarios":"Calling 'create-category-group' / insertCategoryGroup with a name matching an existing group case-insensitively, e.g. 'Bills' when 'bills' exists — including when the existing group is hidden.","commonSituations":"Users recreating a group they previously only hid (hidden groups still count); importers/setup scripts re-running and re-adding default groups like 'Bills' or 'Income'; renaming a group to collide with another.","solutions":["Pick a different, unique name for the new category group.","If the existing group is the one you want, unhide/reuse it instead of creating a new one.","If the duplicate is deleted, note tombstoned groups are ignored — the clash is with a live (tombstone = 0) group; rename or restore it."],"exampleFix":"// before\nawait send('create-category-group', { name: 'Bills' }); // 'bills' exists\n// after\nconst groups = await getCategories();\nconst exists = groups.group.some(g => g.name.toLowerCase() === 'bills');\nif (!exists) await send('create-category-group', { name: 'Bills' });","handlingStrategy":"validation","validationCode":"const groups = (await send('get-categories')).group;\nif (groups.some(g => g.name.toLowerCase() === name.toLowerCase())) {\n  throw new Error(`A category group named '${name}' already exists (possibly hidden).`);\n}","typeGuard":null,"tryCatchPattern":"try {\n  await send('create-category-group', { name });\n} catch (e) {\n  if (/category group already exists/.test(e.message)) {\n    console.error(`Name conflict: ${e.message} Reuse or rename the existing group.`);\n  }\n}","preventionTips":["Check existing groups (including hidden ones) case-insensitively before creating.","Make setup/import scripts idempotent — skip creation when the group exists.","Remember hidden groups still reserve their names."],"tags":["duplicate","categories","constraint"],"backgroundTag":"duplicate-name-conflict","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}