Skip to content

perf: PERF-10 add composite indexes for AuditLog, LlmRequest, Card (#846)#888

Merged
Chris0Jeky merged 3 commits intomainfrom
perf/perf-10-db-indexes
Apr 22, 2026
Merged

perf: PERF-10 add composite indexes for AuditLog, LlmRequest, Card (#846)#888
Chris0Jeky merged 3 commits intomainfrom
perf/perf-10-db-indexes

Conversation

@Chris0Jeky
Copy link
Copy Markdown
Owner

@Chris0Jeky Chris0Jeky commented Apr 16, 2026

Summary

Adds composite indexes via EF Core migration (20260416161303_AddPerfIndexes):

  • IX_Cards_BoardId_ColumnId on Cards (BoardId, ColumnId) — replaces the single-column IX_Cards_BoardId, which SQLite can satisfy via the leftmost prefix of the composite. The FK_Cards_Boards_BoardId constraint remains enforced by the FOREIGN KEY schema itself (not the index). IX_Cards_ColumnId is retained.
  • IX_AuditLogs_UserId_Timestamp on AuditLogs (UserId, Timestamp) — accelerates AuditLogRepository.GetByUserAsync (filters by UserId and orders by Timestamp DESC).
  • IX_AuditLogs_EntityId_Timestamp on AuditLogs (EntityId, Timestamp) — accelerates AuditLogRepository.GetByBoardAsync.

Deviations from the issue AC

  • AuditLog (BoardId, Timestamp)(EntityId, Timestamp): AuditLog has no BoardId column; board scope is resolved polymorphically via EntityId. The composite on (EntityId, Timestamp) is the functional analogue and serves the same query path. Adding a literal BoardId column was out of scope for PERF-10.
  • IX_LlmRequests_UserId_Status not created: Already present pre-PR (see LlmRequestConfiguration.cs:59, builder.HasIndex(lr => new { lr.UserId, lr.Status })). Re-declaring would create a duplicate IX_LlmRequests_UserId_Status1.

Ordering semantics (SQLite + EF Core 8)

Descending-timestamp ordering on an index requires EF Core 9 (IsDescending()), which the repo does not use. The indexes are declared unordered; SQLite's query planner performs a backward range scan for ORDER BY Timestamp DESC over the composite, which is acceptable for local-first SQLite workloads.

Snapshot drift picked up

The regenerated snapshot also records IsConcurrencyToken() on AutomationProposal.UpdatedAt, which was already declared in the entity configuration (main commit 9f7dc936) but absent from the previous snapshot. This is a model-only annotation (no schema change on SQLite); no AlterColumn is required.

Closes #846

Test plan

  • Backend unit + integration suites pass (Integration: 20/20, Architecture: 8/8, Api migration subset: 14/14)
  • Migration applies cleanly to a fresh SQLite DB (covered by Integration tests)
  • Snapshot stays in sync with entity configurations

Adds composite HasIndex declarations for PERF-10 (#846):
- AuditLog: (UserId, Timestamp) and (EntityId, Timestamp)
- Card: (BoardId, ColumnId)

LlmRequest (UserId, Status) already existed from 20260211082334.
Adds the three new composite indexes from PERF-10 (#846):
- IX_AuditLogs_UserId_Timestamp
- IX_AuditLogs_EntityId_Timestamp (board-scope analogue; AuditLog has no
  BoardId column, board queries filter via EntityId)
- IX_Cards_BoardId_ColumnId (also covers the old IX_Cards_BoardId, which
  EF therefore drops)

IX_LlmRequests_UserId_Status already existed since 20260211082334.
@Chris0Jeky
Copy link
Copy Markdown
Owner Author

Adversarial self-review

Re-read the migration, entity configs, and snapshot as an EF Core reviewer. Focused on: is the migration complete, are column names real, does it duplicate existing indexes, does the snapshot stay in sync, and does EF Core 8 accept the syntax used.

Findings

  • [none, verified] AuditLog.BoardId does not exist on the domain entity — it only exposes EntityId (polymorphic audit target). The issue's AC mentioned BoardId_Timestamp aspirationally; the migration correctly substitutes IX_AuditLogs_EntityId_Timestamp which is what actually exists. Documented this substitution would strengthen the PR body — will add a note.
  • [none, verified] IX_LlmRequests_UserId_Status already exists in LlmRequestConfiguration.cs (pre-existing builder.HasIndex(lr => new { lr.UserId, lr.Status })). The agent correctly did not duplicate it — confirming this avoided an IX_LlmRequests_UserId_Status1 suffix collision.
  • [none, verified] EF Core 8 compatibility. No use of EF9-only IsDescending(). SQLite will reverse-scan on ORDER BY Timestamp DESC queries over the unordered composite; acceptable for local-first workloads.
  • [none, verified] Snapshot sync. TaskdeckDbContextModelSnapshot.cs edits match the new HasIndex calls in AuditLogConfiguration.cs and CardConfiguration.cs.
  • [none, verified] Migration name/timestamp. 20260416161303_AddPerfIndexes is chronologically latest; no collision with prior migrations.
  • [none, verified] Dropped IX_Cards_BoardId. The migration drops the single-column IX_Cards_BoardId before creating the composite IX_Cards_BoardId_ColumnId. A single-column prefix index is redundant with the composite (SQLite can use the first-column prefix), so this is correct.

CI status

Will monitor after posting.

Acted on

  • No implementation defects. Will update PR description to call out the BoardId → EntityId substitution for AuditLog, and to note that LlmRequests_UserId_Status already existed.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces performance optimizations by adding composite indexes to the AuditLogs and Cards tables to improve query efficiency. The migration file includes logic to drop redundant single-column indexes in favor of these new composite indexes. However, the migration appears incomplete as it lacks the necessary AlterColumn operation for the AutomationProposal.UpdatedAt concurrency token shown in the model snapshot. Additionally, there is a contradiction between the code comments in CardConfiguration.cs and the actual migration implementation regarding the retention of the IX_Cards_BoardId index.

Comment on lines +11 to +31
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_Cards_BoardId",
table: "Cards");

migrationBuilder.CreateIndex(
name: "IX_Cards_BoardId_ColumnId",
table: "Cards",
columns: new[] { "BoardId", "ColumnId" });

migrationBuilder.CreateIndex(
name: "IX_AuditLogs_EntityId_Timestamp",
table: "AuditLogs",
columns: new[] { "EntityId", "Timestamp" });

migrationBuilder.CreateIndex(
name: "IX_AuditLogs_UserId_Timestamp",
table: "AuditLogs",
columns: new[] { "UserId", "Timestamp" });
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The Up method is missing operations that are reflected in the model snapshot and designer files, indicating an incomplete migration:

  1. Concurrency Token: AutomationProposal.UpdatedAt is marked as a concurrency token in the snapshot (line 410) and designer (line 413), but there is no AlterColumn operation here. This will cause drift and potential issues with future migrations.
  2. Missing Index: The PR summary mentions adding IX_LlmRequests_UserId_Status, but this index is not created in the Up method. It also doesn't appear as a change in the snapshot diff, suggesting it was already present or the configuration change was omitted.

Please regenerate the migration to ensure it captures all intended model changes.

Comment on lines +48 to +52
// PERF-10: composite index for board+column lookups (the default board
// load path filters by BoardId and then groups by ColumnId). SQLite can
// satisfy single-column filters on BoardId from this composite too, but
// we retain the existing IX_Cards_BoardId / IX_Cards_ColumnId indexes to
// avoid changing FK index conventions.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The comment contradicts the migration implementation. AddPerfIndexes.cs drops IX_Cards_BoardId at line 13, but this comment says it is retained. While dropping the redundant index is correct (as the composite index covers it), the documentation should be updated to reflect the actual state.

        // PERF-10: composite index for board+column lookups (the default board
        // load path filters by BoardId and then groups by ColumnId). SQLite can
        // satisfy single-column filters on BoardId from this composite too, so
        // the redundant single-column IX_Cards_BoardId is dropped in the migration
        // while IX_Cards_ColumnId is retained.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds EF Core model configuration + migration changes to improve query performance by introducing new composite indexes (primarily for AuditLogs and Cards) and updating the EF model snapshot accordingly.

Changes:

  • Add composite index IX_Cards_BoardId_ColumnId and migration logic that replaces the prior IX_Cards_BoardId single-column index.
  • Add composite indexes for AuditLogs: IX_AuditLogs_UserId_Timestamp and IX_AuditLogs_EntityId_Timestamp.
  • Update EF Core model snapshot and add the corresponding migration + designer output.

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
backend/src/Taskdeck.Infrastructure/Persistence/Configurations/CardConfiguration.cs Adds composite Cards index configuration + explanatory PERF-10 comment.
backend/src/Taskdeck.Infrastructure/Persistence/Configurations/AuditLogConfiguration.cs Adds composite AuditLogs indexes for user and entity history queries.
backend/src/Taskdeck.Infrastructure/Migrations/TaskdeckDbContextModelSnapshot.cs Snapshot updated to reflect new composite indexes (and removal of Cards BoardId index).
backend/src/Taskdeck.Infrastructure/Migrations/20260416161303_AddPerfIndexes.cs Migration creates the new indexes and drops IX_Cards_BoardId.
backend/src/Taskdeck.Infrastructure/Migrations/20260416161303_AddPerfIndexes.Designer.cs Generated designer reflecting the updated model.
Files not reviewed (1)
  • backend/src/Taskdeck.Infrastructure/Migrations/20260416161303_AddPerfIndexes.Designer.cs: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +50 to +52
// satisfy single-column filters on BoardId from this composite too, but
// we retain the existing IX_Cards_BoardId / IX_Cards_ColumnId indexes to
// avoid changing FK index conventions.
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says we “retain the existing IX_Cards_BoardId / IX_Cards_ColumnId indexes”, but this change set actually removes the single-column BoardId index (see AddPerfIndexes migration + snapshot). Either update this comment to reflect the new indexing strategy (composite replaces BoardId index), or explicitly keep the BoardId index in the model/migration if that’s the intent.

Suggested change
// satisfy single-column filters on BoardId from this composite too, but
// we retain the existing IX_Cards_BoardId / IX_Cards_ColumnId indexes to
// avoid changing FK index conventions.
// also satisfy single-column filters on BoardId from the leftmost prefix
// of this composite index. We do not add a separate BoardId index here,
// and we leave FK convention indexes such as ColumnId unchanged.

Copilot uses AI. Check for mistakes.
Comment on lines +13 to +20
migrationBuilder.DropIndex(
name: "IX_Cards_BoardId",
table: "Cards");

migrationBuilder.CreateIndex(
name: "IX_Cards_BoardId_ColumnId",
table: "Cards",
columns: new[] { "BoardId", "ColumnId" });
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Up() drops the existing IX_Cards_BoardId index. That contradicts the rationale comment in CardConfiguration (and the PR description) which says the existing BoardId index is retained. If the intent is only to add a composite index, avoid dropping IX_Cards_BoardId; if the intent is to replace it, please update the PR description/comments to match so consumers don’t assume both indexes exist.

Copilot uses AI. Check for mistakes.
Comment on lines +57 to +62
// Note: AuditLog has no BoardId column; board scope is resolved via
// EntityId (see AuditLogRepository.GetByBoardAsync). A composite on
// (EntityId, Timestamp) accelerates that ORDER BY Timestamp DESC pattern
// and serves as the board-scoped analogue named for in PERF-10.
builder.HasIndex(al => new { al.EntityId, al.Timestamp })
.HasDatabaseName("IX_AuditLogs_EntityId_Timestamp");
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR/issue summary calls for a board-scoped composite index (BoardId, Timestamp), but the schema/config adds (EntityId, Timestamp) named IX_AuditLogs_EntityId_Timestamp instead. If board scoping is intentionally via EntityId, please align the PR description/issue acceptance criteria and consider whether the index/name should explicitly reflect EntityId semantics to avoid confusion; otherwise, implement the intended BoardId-based index (which would require adding a BoardId column).

Copilot uses AI. Check for mistakes.
Comment on lines +17 to +30
migrationBuilder.CreateIndex(
name: "IX_Cards_BoardId_ColumnId",
table: "Cards",
columns: new[] { "BoardId", "ColumnId" });

migrationBuilder.CreateIndex(
name: "IX_AuditLogs_EntityId_Timestamp",
table: "AuditLogs",
columns: new[] { "EntityId", "Timestamp" });

migrationBuilder.CreateIndex(
name: "IX_AuditLogs_UserId_Timestamp",
table: "AuditLogs",
columns: new[] { "UserId", "Timestamp" });
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This migration is described as adding four composite indexes (including LlmRequests UserId+Status and AuditLogs BoardId+Timestamp), but Up() only creates three (Cards BoardId+ColumnId, AuditLogs EntityId+Timestamp, AuditLogs UserId+Timestamp) and does not touch LlmRequests. If the LlmRequests index already existed previously, update the PR description accordingly; if it’s still required for some environments, it should be created here explicitly.

Copilot uses AI. Check for mistakes.
Prior comment said the single-column IX_Cards_BoardId was retained, but the
AddPerfIndexes migration actually drops it in favor of the composite
IX_Cards_BoardId_ColumnId (which SQLite uses for leftmost-prefix BoardId
scans). The FK constraint remains enforced by the FOREIGN KEY schema itself,
and IX_Cards_ColumnId is untouched. Update the comment to match reality.

Addresses gemini-code-assist + Copilot review on PR #888.
@Chris0Jeky
Copy link
Copy Markdown
Owner Author

Bot-findings triage + fresh adversarial pass

Fixes committed

  • CardConfiguration.cs comment — Updated to match reality: the composite IX_Cards_BoardId_ColumnId replaces the dropped IX_Cards_BoardId (the prior comment incorrectly said both were retained). Addresses gemini-code-assist's medium-priority comment and Copilot's suggestion on line 52. (b294a689)
  • PR body — Rewritten to explicitly document the two aspirational-vs-implemented deviations: AuditLog uses (EntityId, Timestamp) because AuditLog.BoardId does not exist on the domain entity (polymorphic scoping via EntityId), and IX_LlmRequests_UserId_Status was pre-existing. Addresses Copilot's AuditLogs/LlmRequests concerns.

Gemini high-priority flag (rejected as false positive, with rationale)

AutomationProposal.UpdatedAt is marked as a concurrency token in the snapshot but there is no AlterColumn operation — will cause drift.

This is incorrect for EF Core 8 + SQLite:

  • IsConcurrencyToken() is a model-only annotation — it affects EF's change-tracking and UPDATE WHERE clauses, not the column schema. SQLite has no rowversion/timestamp type to alter to.
  • The entity configuration has .IsConcurrencyToken() on AutomationProposal.UpdatedAt since origin/main commit 9f7dc936 (2026-04-09), but the previously committed snapshot (20260409181436_AddOAuthAuthCodes.Designer.cs) did not record it — pre-existing drift, not caused by this PR.
  • This PR's regenerated snapshot picks up that drift correctly. No AlterColumn is emitted because none is needed. Verified: no migration between 9f7dc936 and this PR emitted an AlterColumn for AutomationProposals.UpdatedAt.

Fresh adversarial findings (all cleared)

  • EF Core 8 compatibility: no use of EF9-only IsDescending(). ✓
  • Snapshot ↔ entity configs: IX_AuditLogs_* and IX_Cards_BoardId_ColumnId match across TaskdeckDbContextModelSnapshot.cs + designer + migration. ✓
  • Migration Up/Down symmetric: Down() drops the three new indexes and re-creates the original single-column IX_Cards_BoardId. ✓
  • FK on Cards.BoardId: FK_Cards_Boards_BoardId is enforced by the FOREIGN KEY schema in SQLite, not the index. Dropping the single-column convention index does not break FK enforcement; the composite's leftmost prefix still supports the query planner. ✓
  • Index name collisions: grep across all migrations + snapshot shows the three new names only appear in this PR. No IX_*1 suffix risk. ✓
  • Migration applies cleanly to a fresh DB: verified via Taskdeck.Integration.Tests (20/20 passed locally), Taskdeck.Architecture.Tests (8/8), and migration/schema/audit/board subset of Taskdeck.Api.Tests (14/14). ✓

Final CI status (before fix push)

All 14 required checks green on 4be110c3. New commit b294a689 pushed; will re-verify CI on push.

Chris0Jeky added a commit that referenced this pull request Apr 16, 2026
Adds a delivery entry for the 8 PROD-00 PRs merged on 2026-04-16
(#884 SEC-28, #885 DOC-06, #887 DOC-07, #886 PERF-09, #888 PERF-10,
#889 OPS-29, #890 FE-15, #891 FE-14) with round-2 adversarial review
findings: BREACH JWT-in-body correction (compression level Optimal ->
Fastest), IPv6/IPv4 healthcheck fix, null-throw sentinel fix, skipRetry
opt-out for baseline tests, setpriv entrypoint for upgrade-safe volume
ownership. Also bumps the Last Updated date.
Chris0Jeky added a commit that referenced this pull request Apr 16, 2026
…layer error coverage

Adds a PROD-00 Production-Readiness Round-2 Wave section covering:
- ResponseCompressionApiTests (#886, +3 tests)
- migration-only context for composite DB indexes (#888)
- container hardening verification (no unit tests, docker inspect path)
- HTTP retry with backoff tests + skipRetry opt-out pattern for future
  test authors (#890)
- ErrorBoundary + errorReporting tests + three-layer error coverage
  pattern documenting outer/inner/window layers (#891)

Updates Current Verified Totals to reflect the new test deltas.
Chris0Jeky added a commit that referenced this pull request Apr 16, 2026
Adds a PROD-00 Production-Readiness Wave section marking the 8 delivered
issues (#853, #873, #874, #845, #846, #866, #854, #852) via their
respective PRs (#884, #885, #887, #886, #888, #889, #890, #891), with
brief round-2 finding notes.
@Chris0Jeky Chris0Jeky merged commit cfef530 into main Apr 22, 2026
26 checks passed
@github-project-automation github-project-automation Bot moved this from Pending to Done in Taskdeck Execution Apr 22, 2026
@Chris0Jeky Chris0Jeky deleted the perf/perf-10-db-indexes branch April 22, 2026 00:04
Chris0Jeky added a commit that referenced this pull request Apr 22, 2026
Adds a delivery entry for the 8 PROD-00 PRs merged on 2026-04-16
(#884 SEC-28, #885 DOC-06, #887 DOC-07, #886 PERF-09, #888 PERF-10,
#889 OPS-29, #890 FE-15, #891 FE-14) with round-2 adversarial review
findings: BREACH JWT-in-body correction (compression level Optimal ->
Fastest), IPv6/IPv4 healthcheck fix, null-throw sentinel fix, skipRetry
opt-out for baseline tests, setpriv entrypoint for upgrade-safe volume
ownership. Also bumps the Last Updated date.
Chris0Jeky added a commit that referenced this pull request Apr 22, 2026
…layer error coverage

Adds a PROD-00 Production-Readiness Round-2 Wave section covering:
- ResponseCompressionApiTests (#886, +3 tests)
- migration-only context for composite DB indexes (#888)
- container hardening verification (no unit tests, docker inspect path)
- HTTP retry with backoff tests + skipRetry opt-out pattern for future
  test authors (#890)
- ErrorBoundary + errorReporting tests + three-layer error coverage
  pattern documenting outer/inner/window layers (#891)

Updates Current Verified Totals to reflect the new test deltas.
Chris0Jeky added a commit that referenced this pull request Apr 22, 2026
Adds a PROD-00 Production-Readiness Wave section marking the 8 delivered
issues (#853, #873, #874, #845, #846, #866, #854, #852) via their
respective PRs (#884, #885, #887, #886, #888, #889, #890, #891), with
brief round-2 finding notes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

PERF-10: Add missing database indexes (AuditLog, LlmRequest, Card)

2 participants