You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

5.4 KiB

+++ title = "Filter quotes by date range (before/after with year/month/day granularity)" priority = 5 status = "done" ticket_type = "feature" dependencies = [] +++

Feature

Extend GET /api/quotes with date range filtering. Users can specify a "before" and/or "after" bound, with year/month/day granularity for each. Quotes without a date (date IS NULL) are excluded when a date filter is active.


Query parameter design

Six optional query params are added:

Param Type Description
date_after_year u16 Only include quotes dated on or after this year
date_after_month u8 (112) Narrows after-bound to this month
date_after_day u8 (131) Narrows after-bound to this day
date_before_year u16 Only include quotes dated on or before this year
date_before_month u8 (112) Narrows before-bound to this month
date_before_day u8 (131) Narrows before-bound to this day

The handler constructs two partial ISO strings from these fields before calling list_quotes:

  • after bound (>=): YYYY, YYYY-MM, or YYYY-MM-DD — string comparison anchors to the start of the specified period (e.g., 2020 means ≥ 2020-01-01 in text comparison, but actually >= '2020' which works correctly since any 2020 date starts with 2020).
  • before bound (<=): YYYY-MM-DD where missing month defaults to 12 and missing day defaults to 31 — so date_before_year=2020 means <= '2020-12-31'.

Validation rules:

  • Month must be 112 if provided (without its year, it is an error).
  • Day must be 131 if provided (without its year+month, it is an error).
  • Return 400 Bad Request for invalid combinations.

Part 1 — API: handlers/mod.rs

Extend ListParams:

#[derive(Debug, Deserialize)]
struct ListParams {
    #[serde(default = "default_page")]
    page: u32,
    author: Option<String>,
    tag: Option<String>,
    // Date range filter
    date_after_year:   Option<u16>,
    date_after_month:  Option<u8>,
    date_after_day:    Option<u8>,
    date_before_year:  Option<u16>,
    date_before_month: Option<u8>,
    date_before_day:   Option<u8>,
}

In list_handler, construct the date_after and date_before strings before calling repo.list_quotes:

fn build_date_bound(year: Option<u16>, month: Option<u8>, day: Option<u8>) -> Option<String> {
    match (year, month, day) {
        (None, _, _) => None,
        (Some(y), None, _) => Some(format!("{y:04}")),
        (Some(y), Some(m), None) => Some(format!("{y:04}-{m:02}")),
        (Some(y), Some(m), Some(d)) => Some(format!("{y:04}-{m:02}-{d:02}")),
    }
}

Return 400 if month is present without year, or day is present without year+month.


Part 2 — Repository trait: db/mod.rs

Extend list_quotes signature:

async fn list_quotes(
    &self,
    page: u32,
    author: Option<&str>,
    tag: Option<&str>,
    date_after: Option<&str>,    // new
    date_before: Option<&str>,   // new
) -> Result<ListResult, DbError>;

Part 3 — Native implementation: db/native.rs

In NativeRepository::list_quotes, build the WHERE clause dynamically. Current query filters on author and tag via subquery. Extend it:

-- Additional clauses appended when filters are present:
AND q.date IS NOT NULL
AND q.date >= ?          -- when date_after is Some
AND q.date <= ?          -- when date_before is Some

Use string comparison — ISO YYYY-MM-DD format sorts lexicographically, so >=/<= on the date TEXT column is correct. Prefix matching (e.g. date >= '2020' with date = '2020-06-15') works because '2020-06-15' >= '2020' is true in SQLite string comparison.


Part 4 — D1 implementation: db/d1.rs

Apply the same WHERE clause extensions to the D1 query builder.


Part 5 — UI: Browse page

File: src/bin/ui/pages/browse.rs

Add date filter controls to the filter panel alongside author and tag filters:

  • "After:" year input (<input type="number" min="0" max="9999">), optional month select (112), optional day input.
  • "Before:" same.

On filter apply, include the populated fields as query params. When fields are empty, omit them.


Part 6 — Mock repo in tests: handlers/mod.rs

Update MockRepo::list_quotes to accept the two new date_after / date_before params (ignore them in the mock — just extend the signature).


Part 7 — OpenAPI spec: api/openapi.yaml

Add the six new query parameters to the GET /api/quotes operation with descriptions and schema: {type: integer}.


Files touched

  • src/bin/api/handlers/mod.rsListParams + list_handler + MockRepo
  • src/bin/api/db/mod.rsQuoteRepository::list_quotes signature
  • src/bin/api/db/native.rs — SQL query extension
  • src/bin/api/db/d1.rs — SQL query extension
  • src/bin/ui/pages/browse.rs — date filter UI
  • api/openapi.yaml — new query params

Validation

# From quotesdb/ root
cargo fmt && cargo check && cargo clippy && cargo test
trunk build
redocly lint api/openapi.yaml

Test manually:

  • GET /api/quotes?date_after_year=1900&date_before_year=2000 — should return only quotes with dates in the 20th century.
  • GET /api/quotes?date_after_year=2020&date_after_month=6 — on or after June 2020.
  • GET /api/quotes?date_after_month=3 — should return 400 (month without year).

Commit scope

feat(quotesdb): date range filter for quotes list