{"record":{"id":"09ea46ea20f2c097","repo":"bytebase/bytebase","slug":"project-s-not-found-or-not-marked-as-deleted","errorCode":null,"errorMessage":"project %s not found or not marked as deleted","messagePattern":"project (.+?) not found or not marked as deleted","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"backend/store/project.go","lineNumber":727,"sourceCode":"\trows, err := tx.QueryContext(ctx, sql, args...)\n\tif err != nil {\n\t\treturn errors.Wrapf(err, \"failed to delete projects %s\", projectList)\n\t}\n\tdefer rows.Close()\n\tvar purged []string\n\tfor rows.Next() {\n\t\tvar resourceID string\n\t\tif err := rows.Scan(&resourceID); err != nil {\n\t\t\treturn errors.Wrap(err, \"failed to scan purged project\")\n\t\t}\n\t\tpurged = append(purged, resourceID)\n\t}\n\tif err := rows.Err(); err != nil {\n\t\treturn errors.Wrapf(err, \"failed to read purged projects %s\", projectList)\n\t}\n\tfor _, resourceID := range resourceIDs {\n\t\tif !slices.Contains(purged, resourceID) {\n\t\t\treturn errors.Errorf(\"project %s not found or not marked as deleted\", resourceID)\n\t\t}\n\t}\n\n\tif err := tx.Commit(); err != nil {\n\t\treturn errors.Wrap(err, \"failed to commit transaction\")\n\t}\n\n\t// Publish invalidation only after the purge commits: every captured\n\t// descendant entry is removed while unrelated cache entries survive.\n\ts.removeProjectDescendantCaches(cacheKeys, resourceIDs)\n\n\treturn nil\n}\n\nfunc GetListProjectFilter(workspace, filter string) (*qb.Query, error) {\n\tif filter == \"\" {\n\t\treturn nil, nil\n\t}","sourceCodeStart":709,"sourceCodeEnd":745,"githubUrl":"https://github.com/bytebase/bytebase/blob/1870550677fe08f0d2a78c07acd27541464eb945/backend/store/project.go#L709-L745","documentation":"DeleteProjects physically purges projects that were previously soft-deleted (marked as deleted). It collects the resourceIDs actually purged in the transaction, then verifies every requested resourceID was purged. If a requested project is missing or not marked as deleted, it aborts with \"project %s not found or not marked as deleted\" and rolls back. This guards against purging live or unknown projects.","triggerScenarios":"Calling DeleteProject or BatchDeleteProjects with a resourceID that either does not exist or whose row is not flagged as soft-deleted at purge time — e.g. purging before the soft-delete committed, or after the row was already physically removed.","commonSituations":"Race between soft-delete and purge; retrying a purge that already succeeded (second pass finds nothing); callers passing wrong or typo'd resourceIDs; concurrent workflows restoring the project between delete and purge.","solutions":["Confirm the project exists and is marked as deleted (SELECT resource_id ... WHERE deleted) before purging","Re-check for typos or stale resourceIDs in the caller's delete request","If the purge already ran once, treat this as success (idempotent retry) rather than an error","Serialize soft-delete and purge in one transaction to avoid the race"],"exampleFix":"// before\nfor _, resourceID := range resourceIDs {\n\tif !slices.Contains(purged, resourceID) {\n\t\treturn errors.Errorf(\"project %s not found or not marked as deleted\", resourceID)\n\t}\n}\n// after\nfor _, resourceID := range resourceIDs {\n\tif !slices.Contains(purged, resourceID) {\n\t\tif exists, deleted, err := projectState(ctx, tx, resourceID); err == nil && !exists {\n\t\t\tcontinue // already purged; idempotent\n\t\t} else if err == nil && !deleted {\n\t\t\treturn errors.Errorf(\"project %s not marked as deleted\", resourceID)\n\t\t}\n\t\treturn errors.Errorf(\"project %s not found or not marked as deleted\", resourceID)\n\t}\n}","handlingStrategy":"validation","validationCode":"// before purging, verify the project is soft-deleted\nvar deleted bool\nerr := db.QueryRowContext(ctx,\n\t`SELECT EXISTS(SELECT 1 FROM project WHERE resource_id = $1 AND deleted = TRUE)`,\n\tresourceID).Scan(&deleted)\nif err != nil || !deleted {\n\treturn errors.Errorf(\"project %s not marked as deleted; skip purge\", resourceID)\n}","typeGuard":null,"tryCatchPattern":"if err := store.DeleteProjects(ctx, resourceIDs); err != nil {\n\tif strings.Contains(err.Error(), \"not found or not marked as deleted\") {\n\t\t// already purged or wrong id: treat as idempotent success after verification\n\t\treturn nil\n\t}\n\treturn err\n}","preventionTips":["Verify soft-deleted state before issuing purges","Make purge retries idempotent (skip already-purged ids)","Serialize soft-delete and purge to avoid racing restores"],"tags":["database","delete","consistency","projects"],"backgroundTag":"record-not-found","analyzedSha":"1870550677fe08f0d2a78c07acd27541464eb945","analyzedAt":"2026-09-06T21:16:13.665Z","contentChangedAt":"2026-09-06T21:16:13.665Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}