adding support to the where handler for jsonb for date ranges in the …#83
adding support to the where handler for jsonb for date ranges in the …#83jakesanders wants to merge 10 commits into
Conversation
- Slim down README to focus on quick start and usage example - Move detailed filter documentation to docs/filters.md - Move sort documentation to docs/sorts.md - Add docs/api.md for consumer API reference - Add docs/contributing.md for development setup Signed-off-by: Jake Sanders <jake@procore.com>
Signed-off-by: Matt Lewis <34316418+mattl3w1s@users.noreply.github.com> Signed-off-by: Jake Sanders <jake@procore.com>
…same format as other range filters Signed-off-by: Jake Sanders <jake@procore.com>
Signed-off-by: Matt Lewis <mlewis0260@gmail.com> Signed-off-by: Jake Sanders <jake@procore.com>
Cover the jsonb array condition so future changes keep using the incoming array value in the containment predicate. AI-Authored-By: cursor Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: Matt Lewis <mlewis0260@gmail.com> Signed-off-by: Jake Sanders <jake@procore.com>
Pin development tests to Minitest 5 and load mock support explicitly so the rebased suite can run with existing mock-based tests. AI-Authored-By: cursor Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: Matt Lewis <mlewis0260@gmail.com> Signed-off-by: Jake Sanders <jake@procore.com>
AI-Authored-By: cursor Signed-off-by: Matt Lewis <mlewis0260@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: Jake Sanders <jake@procore.com>
Pin development tests to Minitest 5 and load mock support explicitly so the rebased suite can run with existing mock-based tests. AI-Authored-By: cursor Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: Matt Lewis <mlewis0260@gmail.com>
AI-Authored-By: cursor Signed-off-by: Matt Lewis <mlewis0260@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: Jake Sanders <jake@procore.com>
37f70f9 to
a7b0a17
Compare
| end | ||
|
|
||
| def date_range?(val) | ||
| val.is_a?(String) && val.include?("...") |
There was a problem hiding this comment.
This potentially blocks the default behavior if there was a key / value with a value containing "..." which was not intended to be a date range. Suggested improvement: catch the StandardError from normalized_date_range and fall back to the default behavior in the else.
| "#{@param.internal_name}->>'#{key}' #{element === nil ? 'IS NULL' : "= :value_#{i}"}" | ||
| end.join(' OR ') | ||
| collection.where("(#{main_condition}) OR (#{sub_conditions})", elements) | ||
| elsif date_range?(val) |
There was a problem hiding this comment.
This seems legit. From my agent:
====
[blocking] correctness / integration
This branch may be unreachable for real requests. Filter#parameterize calls ValueParser before passing the value to WhereHandler. For :jsonb, Parameter#supports_ranges? returns true (only :string, :text, :scope are excluded — see parameter.rb:31).
ValueParser#parse_as_range? checks supports_ranges && raw_value.to_s.include?("..."). Two realistic request shapes both fail:
- Nested params (
filters[metadata][published_at]=2018-01-01...2018-01-02) → Rails gives a Hash like{"published_at"=>"2018-01-01...2018-01-02"}.hash.to_s.include?("...")istrue, soparse_as_range?fires and callsvalue.split("...")on the Hash →NoMethodError. - JSON string (
filters[metadata]={"published_at":"2018-01-01...2018-01-02"}) →parse_as_range?fires on the whole JSON string, producing aRangefrom the first two...-separated JSON fragments. That Range reachesapply_jsonb_conditions, which iterates it viaString#succ— the latent bug described in issue #80.
The tests pass because they call handler.call directly, bypassing ValueParser. An integration test through Sift::Filtrator.filter (or the dummy controller) would expose this.
Fix: either guard parse_as_range? to skip when the value is a Hash, or handle the jsonb range detection inside ValueParser#parse_json_and_values so each individual key value is tested for the ... pattern rather than the entire outer value.
| collection.where("(#{main_condition}) OR (#{sub_conditions})", elements) | ||
| elsif date_range?(val) | ||
| from_date, end_date = normalized_date_range(val) | ||
| collection.where("#{@param.internal_name}->>'#{key}' BETWEEN ? AND ?", from_date, end_date) |
There was a problem hiding this comment.
Good idea to add the timestampz type casting from agent (below)
======
[blocking] correctness
->> extracts the JSONB key as text. The BETWEEN comparison is therefore lexicographic, not type-aware:
- Dates: Works only when all stored values are ISO-8601 strings with the same timezone offset.
'2018-01-01T00:00:00+05:30'and'2018-01-01T00:00:00Z'represent the same instant but compare differently as text. - Numerics:
'9' > '10'lexicographically, so a range like"9...100"would return wrong results.
Issue #80's acceptance criteria explicitly calls for type-casting: (metadata->>'price')::numeric BETWEEN 10 AND 100. The PR implements none of that — no key-type declaration, no cast in the SQL.
For datetime specifically, the cast would be:
(metadata->>'published_at')::timestamptz BETWEEN ? AND ?This requires the caller to declare the key type (part of the feature spec that was left unimplemented).
| elsif date_range?(val) | ||
| from_date, end_date = normalized_date_range(val) | ||
| collection.where("#{@param.internal_name}->>'#{key}' BETWEEN ? AND ?", from_date, end_date) |
There was a problem hiding this comment.
Probably also legit
====
[warning] semantics
SQL BETWEEN a AND b is inclusive on both ends. Sift's ... separator follows Ruby's exclusive-range convention (exclusive upper bound). Issue #80 explicitly calls this out as an open question — the PR resolves it silently in favor of inclusive semantics without any documentation or changelog entry.
At minimum, add a comment explaining the deliberate choice. Ideally document it in the README alongside the other range filter docs so callers know "2018-01-01...2018-01-02" on a JSONB key matches records including 2018-01-02, unlike what the ... literal implies in Ruby.
| end_date = "2018-01-02T00:00:00+00:00" | ||
| range_string = "#{start_date}...#{end_date}" | ||
|
|
||
| relation = handler.call(Post.all, { "published_at" => range_string }, {}, []) |
There was a problem hiding this comment.
Good suggestion
====
[warning] testing
This test calls handler.call directly with a pre-parsed Hash, bypassing Filter#parameterize → ValueParser. As noted in the inline comment on where_handler.rb:29, ValueParser#parse_as_range? would intercept the value before WhereHandler runs in a real request, making this code path unreachable in production.
Adding an integration test through Sift::Filtrator.filter (or the existing dummy PostsController) would catch that gap:
test "jsonb date range filter works end-to-end via Filtrator" do
filters = [Sift::Filter.new(:metadata, :jsonb, :metadata, nil)]
range_string = "2018-01-01T00:00:00+00:00...2018-01-02T00:00:00+00:00"
params = { filters: { metadata: { "published_at" => range_string } } }
relation = Sift::Filtrator.filter(Post.all, params, filters)
assert_includes relation.to_sql, "BETWEEN"
end
…same format as other range filters
#80
Checklist: