Skip to content

feat: add inclusive date ranges to habit logs - #822

Merged
thomasluizon merged 1 commit into
mainfrom
thomasluizon/ticket-387-habit-log-range
Oct 20, 2026
Merged

thomasluizon merged 1 commit into
mainfrom
thomasluizon/ticket-387-habit-log-range

Conversation

@thomasluizon

Copy link
Copy Markdown
Owner

Closes thomasluizon/orbit-tickets#387

GET /api/habits/{id}/logs accepts optional ISO from and to dates. Supplying both returns every visible log in that inclusive range, including history older than a year and ranges containing more than 1,000 rows. Omitting both preserves the existing user-local 365-day lookback, 1,000-row cap, ordering, and response fields.

Implementation

  • HabitsController.cs forwards the dates. ModelBinding/IsoDateQueryModelBinder.cs strictly binds ISO calendar dates and routes malformed values through the existing ValidationExceptionHandler envelope.
  • GetHabitLogsQuery.cs and its FluentValidation validator select and validate the explicit range. IHabitLogReader.cs and HabitLogReader.cs add a separate SQL-filtered range read so the legacy recent-read implementation stays intact. Authorization remains ahead of either read. This is a read operation with no domain entity mutation.
  • Unit tests cover historical results beyond the legacy cap, inclusive bounds, ordering, deleted rows, habit ownership, cancellation, default behavior, malformed dates, incomplete pairs, reversed ranges, and the maximum range boundary.
  • Regenerated both OpenAPI documents and the local architecture map. The generated architecture files are excluded from the commit as required.

Assumptions

  • An explicit range requires both dates; reject a single bound rather than infer an open-ended range.
  • The 366-day limit counts both inclusive endpoints; accept 366 calendar dates rather than a 366-day difference containing 367 calendar dates.
  • Keep the recent-read port and add ReadRangeLogsAsync rather than broaden the existing method, preserving the legacy query shape and callers.

Test evidence

Before changing behavior, the existing tests passed with the defect present:

dotnet test tests/Orbit.Application.Tests --filter FullyQualifiedName~GetHabitLogsQueryHandlerTests --verbosity quiet
dotnet test tests/Orbit.Infrastructure.Tests --filter FullyQualifiedName~HabitLogReaderTests --verbosity quiet

Results: 7 handler tests and 19 reader tests passed. The reader run had only the optional query fields declared; the handler and reader behavior remained unchanged.

Added HabitLogReaderTests.Handle_ExplicitPastRange_ReturnsOnlyInclusiveRangeWithoutLegacyCap, composing the actual handler, repository, and reader over EF InMemory. Before fixing the implementation:

dotnet test tests/Orbit.Infrastructure.Tests --filter FullyQualifiedName~Handle_ExplicitPastRange_ReturnsOnlyInclusiveRangeWithoutLegacyCap --verbosity quiet

Result: exit 1, one failure. January 2025 returned an empty collection instead of the expected 1,002 rows with user-today in July 2026. The same command passed after the fix. The strengthened regression also excludes both adjacent dates, another habit's rows, and a soft-deleted row.

Post-commit verification:

  • dotnet build Orbit.slnx --verbosity quiet: exit 0, zero errors.
  • dotnet test tests/Orbit.Application.Tests --no-build --filter FullyQualifiedName~GetHabitLogsQuery --verbosity quiet: 19 passed.
  • dotnet test tests/Orbit.Infrastructure.Tests --no-build --filter 'FullyQualifiedName~HabitLogReaderTests|FullyQualifiedName~HabitLogRangeContractTests|FullyQualifiedName~HabitsControllerTests.GetLogs' --verbosity quiet: 38 passed.
  • dotnet test tests/Orbit.Domain.Tests --no-build --verbosity quiet: 767 passed.
  • dotnet test tests/Orbit.Application.Tests --no-build --verbosity quiet: 5,236 passed.
  • dotnet test tests/Orbit.Infrastructure.Tests --no-build --verbosity quiet: 3,788 passed.
  • dotnet test tests/Orbit.Analyzers.Tests --no-build --verbosity quiet: 40 passed.
  • Local oasdiff v1.11.7 breaking comparison against the committed origin/main v1 document with --fail-on ERR: exit 0, no breaking changes.
  • node tools/arch-map.mjs: exit 0. Pre-commit formatting, dash, timeless, root allowlist, and suppression allowlist checks passed.

Query plan

HabitLogReaderTests.ReadRangeLogsAsync_UsesExistingDateIndex executes the actual range read over the shared SQLite in-memory context and runs EXPLAIN QUERY PLAN on the emitted command with its actual parameters. Reproduce with:

dotnet test tests/Orbit.Infrastructure.Tests --filter FullyQualifiedName~ReadRangeLogsAsync_UsesExistingDateIndex --logger 'console;verbosity=detailed'

The emitted query applies HabitId = @habitId AND Date >= @from AND Date <= @to, the existing soft-delete filter, and ORDER BY Date DESC, CreatedAtUtc DESC. Its complete returned plan rows were:

4 | 0 | 49 | SEARCH h USING INDEX IX_HabitLogs_HabitId_Date (HabitId=? AND Date>? AND Date<?)
39 | 0 | 0 | USE TEMP B-TREE FOR LAST TERM OF ORDER BY

The existing index serves the range filter; the secondary timestamp ordering uses a temporary B-tree. No index or migration was added. This is SQLite evidence, not a PostgreSQL production plan. A PostgreSQL EXPLAIN was not collected under the work order's prohibition on starting servers.

External interface evidence

HabitLogRangeContractTests uses the installed ASP.NET MVC action descriptors, real model-binder factory, ParameterBinder, and QueryStringValueProvider, rather than mocked binding results. Real from=2025-01-01&to=2025-01-31 values bind to the matching DateOnly values, omitted dates bind to null, and malformed or empty values reach the existing validation envelope. The passing tests exercise the binder's ValueProviderResult.Length and FirstValue reads against the actual provider. The query-plan test copies the actual EF command parameters and enumerates every returned column without assuming named plan fields; the complete returned rows are shown above.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes Reviewed the complete PR diff, the binding and validation pipeline, persistence filters, and the sibling client's route and response schema.

  • Optional date ranges: Adds strict ISO from/to binding and validates paired bounds, ordering, and the 366-date inclusive limit.
  • Historical reads: Adds an inclusive SQL-filtered range read without the legacy row cap, retaining ownership checks, soft-delete filtering, and newest-first ordering.
  • Existing clients: Preserves the default user-local lookback, 1,000-row cap, and response fields; both OpenAPI documents add only optional parameters.
  • Verification: All 57 focused handler, validator, controller, MVC-binding, and persistence tests passed. Query-plan coverage uses SQLite; PostgreSQL execution plans were not independently verified.

Pullfrog  | View workflow run | Using gpt-6.1-sol | 𝕏

@sonarqubecloud

Copy link
Copy Markdown

@thomasluizon
thomasluizon merged commit 906f9e4 into main Oct 20, 2026
27 checks passed
@thomasluizon
thomasluizon deleted the thomasluizon/ticket-387-habit-log-range branch October 20, 2026 03:14
thomasluizon added a commit that referenced this pull request Oct 20, 2026
… and bulk reparenting (#746) (#827)

* fix: expose persisted habit recurrence origins (#820)

(cherry picked from commit 6e4a3a5)

* feat: support bounded habit log date ranges (#822)

(cherry picked from commit 906f9e4)

* Add bulk habit reparenting (#821)

* refactor: share habit hierarchy validation helpers

* feat: move selected habits under a parent atomically

* fix: register bulk parent action in the capability catalog

(cherry picked from commit f67708f)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant