-
-
Notifications
You must be signed in to change notification settings - Fork 89
RG-T41 Fixing Postgres Profile error #526
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| using FluentMigrator; | ||
|
|
||
| namespace Resgrid.Providers.Migrations.Migrations | ||
| { | ||
| /// <summary> | ||
| /// The initial schema created DepartmentProfiles with misspelled "Lo" and "oglePlus" columns (the same | ||
| /// dropped-"go" damage M0113 repaired on Notes and Documents) while the entity, and so every | ||
| /// Dapper-generated INSERT/UPDATE, uses "Logo" and "GooglePlus" — no department profile could ever be | ||
| /// saved against a database built from M0001, and the Department Profile page failed on first load when | ||
| /// it tried to create the row. Renames each column where the typo exists; guarded so databases that | ||
| /// already have the correct column (every database that predates M0001, or one hand-fixed) are untouched. | ||
| /// | ||
| /// No data moves: because every save failed, a database carrying the typo holds no DepartmentProfiles | ||
| /// rows, so M0172's one-time legacy logo copy (which it skipped here, finding no Logo column) has | ||
| /// nothing to pick up. | ||
| /// </summary> | ||
| [Migration(231)] | ||
| public class M0231_FixDepartmentProfilesLogoGooglePlusColumns : Migration | ||
| { | ||
| public override void Up() | ||
| { | ||
| Execute.Sql(@" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Execute.Sql call in Kody rule violation: Add try-catch blocks for external calls try
{
Execute.Sql(@"...");
}
catch (Exception ex)
{
throw new InvalidOperationException("Failed to rename DepartmentProfiles.Logo column during migration M0231.", ex);
}Prompt for LLMTalk to Kody by mentioning @kody Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction. |
||
| IF COL_LENGTH('dbo.DepartmentProfiles', 'Lo') IS NOT NULL AND COL_LENGTH('dbo.DepartmentProfiles', 'Logo') IS NULL | ||
| EXEC sp_rename 'dbo.DepartmentProfiles.Lo', 'Logo', 'COLUMN';"); | ||
|
|
||
| Execute.Sql(@" | ||
| IF COL_LENGTH('dbo.DepartmentProfiles', 'oglePlus') IS NOT NULL AND COL_LENGTH('dbo.DepartmentProfiles', 'GooglePlus') IS NULL | ||
| EXEC sp_rename 'dbo.DepartmentProfiles.oglePlus', 'GooglePlus', 'COLUMN';"); | ||
| } | ||
|
|
||
| public override void Down() | ||
| { | ||
| // One-way typo fix; nothing to restore. | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| using FluentMigrator; | ||
|
|
||
| namespace Resgrid.Providers.MigrationsPg.Migrations | ||
| { | ||
| /// <summary> | ||
| /// The initial schema created departmentprofiles with misspelled "lo" and "ogleplus" columns (the same | ||
| /// dropped-"go" damage M0113 repaired on notes and documents) while the entity, and so every | ||
| /// Dapper-generated INSERT/UPDATE, uses "logo" and "googleplus" — every save against a database built | ||
| /// from M0001 failed with 42703 "column googleplus of relation departmentprofiles does not exist", which | ||
| /// broke the Department Profile page on first load when it tried to create the row. Renames each column | ||
| /// where the typo exists; guarded so databases that already have the correct column (or were hand-fixed) | ||
| /// are untouched. | ||
| /// | ||
| /// No data moves: because every save failed, a database carrying the typo holds no departmentprofiles | ||
| /// rows, so M0172's one-time legacy logo copy (which it skipped here, finding no logo column) has | ||
| /// nothing to pick up. | ||
| /// </summary> | ||
| [Migration(231)] | ||
| public class M0231_FixDepartmentProfilesLogoGooglePlusColumnsPg : Migration | ||
| { | ||
| public override void Up() | ||
| { | ||
| Execute.Sql(@" | ||
| DO $$ | ||
| BEGIN | ||
| IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'departmentprofiles' AND column_name = 'lo') | ||
| AND NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'departmentprofiles' AND column_name = 'logo') THEN | ||
| ALTER TABLE public.departmentprofiles RENAME COLUMN lo TO logo; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The column renames in Kody rule violation: Block risky database migrations (locking ops, downtime risk) ALTER TABLE public.departmentprofiles RENAME COLUMN lo TO logo;Prompt for LLMTalk to Kody by mentioning @kody Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction. |
||
| END IF; | ||
|
|
||
| IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'departmentprofiles' AND column_name = 'ogleplus') | ||
| AND NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'departmentprofiles' AND column_name = 'googleplus') THEN | ||
| ALTER TABLE public.departmentprofiles RENAME COLUMN ogleplus TO googleplus; | ||
|
Comment on lines
+26
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The PostgreSQL migration checks only for -- Detect and repair the actual legacy schema shape used by affected databases;
-- keep the guard consistent with the schema/version that produced the failure.
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'departmentprofiles' AND column_name = '<actual_legacy_logo_column>')
AND NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'departmentprofiles' AND column_name = 'logo') THEN
ALTER TABLE public.departmentprofiles RENAME COLUMN <actual_legacy_logo_column> TO logo;Prompt for LLMTalk to Kody by mentioning @kody Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction. |
||
| END IF; | ||
| END $$;"); | ||
| } | ||
|
|
||
| public override void Down() | ||
| { | ||
| // One-way typo fix; nothing to restore. | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -150,16 +150,37 @@ public async Task Assign_for_review_touches_only_records_awaiting_review_and_rep | |
| _records.Setup(r => r.AssignReviewerAsync(Dept, Exporter, "r1", "reviewer", "rotation", It.IsAny<CancellationToken>())).ReturnsAsync(new RecordAggregate()); | ||
| _records.Setup(r => r.AssignReviewerAsync(Dept, Exporter, "draft", "reviewer", "rotation", It.IsAny<CancellationToken>())).ThrowsAsync(new RecordTransitionException("draft", RmsRecordState.Draft, RmsRecordState.Draft, "only a Record awaiting review can be assigned a reviewer")); | ||
| _records.Setup(r => r.AssignReviewerAsync(Dept, Exporter, "hidden", "reviewer", "rotation", It.IsAny<CancellationToken>())).ThrowsAsync(new UnauthorizedAccessException()); | ||
| // An id with no operational Record (deleted since the list rendered, or another kind's id) must not abort the batch. | ||
| _records.Setup(r => r.AssignReviewerAsync(Dept, Exporter, "missing", "reviewer", "rotation", It.IsAny<CancellationToken>())).ThrowsAsync(new KeyNotFoundException("Record missing does not exist in this department.")); | ||
| _records.Setup(r => r.AssignReviewerAsync(Dept, Exporter, "r2", "reviewer", "rotation", It.IsAny<CancellationToken>())).ReturnsAsync(new RecordAggregate()); | ||
| _authorization.Setup(a => a.HasPermissionAsync("reviewer", Dept, PermissionTypes.ReviewRecords)).ReturnsAsync(true); | ||
|
|
||
| var result = await _service.AssignForReviewAsync(Dept, Exporter, new RecordsBulkAssignRequest { RecordIds = new List<string> { "r1", "draft", "hidden" }, ReviewerUserId = "reviewer", Reason = "rotation" }); | ||
| var result = await _service.AssignForReviewAsync(Dept, Exporter, new RecordsBulkAssignRequest { RecordIds = new List<string> { "r1", "draft", "missing", "hidden", "r2" }, ReviewerUserId = "reviewer", Reason = "rotation" }); | ||
|
|
||
| result.Processed.Should().Be(1); | ||
| result.Skips.Select(s => s.RecordId + ":" + s.Reason).Should().BeEquivalentTo("draft:not_awaiting_review", "hidden:not_visible"); | ||
| result.Processed.Should().Be(2); | ||
| result.Skips.Select(s => s.RecordId + ":" + s.Reason).Should().BeEquivalentTo("draft:not_awaiting_review", "missing:not_found", "hidden:not_visible"); | ||
| result.Run.Should().BeNull(); | ||
|
|
||
| _authorization.Setup(a => a.IsActiveMemberAsync("gone", Dept)).ReturnsAsync(false); | ||
| Func<Task> inactive = () => _service.AssignForReviewAsync(Dept, Exporter, new RecordsBulkAssignRequest { RecordIds = new List<string> { "r1" }, ReviewerUserId = "gone" }); | ||
| await inactive.Should().ThrowAsync<ArgumentException>(); | ||
|
|
||
| // A reviewer without ReviewRecords fails the whole request up front rather than skipping every row. | ||
| Func<Task> notReviewer = () => _service.AssignForReviewAsync(Dept, Exporter, new RecordsBulkAssignRequest { RecordIds = new List<string> { "r1" }, ReviewerUserId = "member" }); | ||
| (await notReviewer.Should().ThrowAsync<ArgumentException>()).Which.Message.Should().Contain("ReviewRecords"); | ||
|
|
||
| // Permission revoked mid-batch: the rows already assigned are still reported rather than lost behind an error. | ||
| _records.Setup(r => r.AssignReviewerAsync(Dept, Exporter, "r2", "reviewer", "rotation", It.IsAny<CancellationToken>())).ThrowsAsync(new ArgumentException("The chosen reviewer does not hold the ReviewRecords permission.")); | ||
| var partial = await _service.AssignForReviewAsync(Dept, Exporter, new RecordsBulkAssignRequest { RecordIds = new List<string> { "r1", "r2" }, ReviewerUserId = "reviewer", Reason = "rotation" }); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unhandled asynchronous failures from AssignForReviewAsync can become unhandled rejections instead of producing test-context failures. Wrap the awaited service call in try/catch and call Assert.Fail with the exception details. Kody rule violation: Handle async operations with proper error handling RecordsBulkAssignResult partial;
try
{
partial = await _service.AssignForReviewAsync(Dept, Exporter, new RecordsBulkAssignRequest { RecordIds = new List<string> { "r1", "r2" }, ReviewerUserId = "reviewer", Reason = "rotation" });
}
catch (Exception ex)
{
Assert.Fail($"AssignForReviewAsync failed unexpectedly: {ex}");
}Prompt for LLMTalk to Kody by mentioning @kody Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction. |
||
| partial.Processed.Should().Be(1); | ||
| partial.Skips.Select(s => s.RecordId + ":" + s.Reason).Should().BeEquivalentTo("r2:reviewer_not_eligible"); | ||
|
|
||
| // A concurrent write on one row is skipped; the rows after it are still assigned. | ||
| _records.Setup(r => r.AssignReviewerAsync(Dept, Exporter, "busy", "reviewer", "rotation", It.IsAny<CancellationToken>())).ThrowsAsync(new RecordConcurrencyException("busy", 3, 4)); | ||
| _records.Setup(r => r.AssignReviewerAsync(Dept, Exporter, "r3", "reviewer", "rotation", It.IsAny<CancellationToken>())).ReturnsAsync(new RecordAggregate()); | ||
| var conflicted = await _service.AssignForReviewAsync(Dept, Exporter, new RecordsBulkAssignRequest { RecordIds = new List<string> { "r1", "busy", "r3" }, ReviewerUserId = "reviewer", Reason = "rotation" }); | ||
| conflicted.Processed.Should().Be(2); | ||
| conflicted.Skips.Select(s => s.RecordId + ":" + s.Reason).Should().BeEquivalentTo("busy:conflict"); | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unhandled authorization exceptions can escape from the awaited _authorization.HasPermissionAsync call when the operation is rejected, preventing the service from handling the authorization failure. Wrap the call in try/catch to handle or log the exception with appropriate context before rethrowing it.
Kody rule violation: Handle async operations with proper error handling
Prompt for LLM
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.