Skip to content

adding support to the where handler for jsonb for date ranges in the …#83

Open
jakesanders wants to merge 10 commits into
mainfrom
handle_jsonb_date_ranges
Open

adding support to the where handler for jsonb for date ranges in the …#83
jakesanders wants to merge 10 commits into
mainfrom
handle_jsonb_date_ranges

Conversation

@jakesanders

Copy link
Copy Markdown

…same format as other range filters

#80

Checklist:

  • I have updated the necessary documentation
  • I have updated the changelog, if necessary (CHANGELOG.md)
  • I have signed off all my commits as required by DCO
  • My build is green

@jakesanders jakesanders requested a review from a team as a code owner June 8, 2026 16:08
rafafloresta and others added 9 commits June 8, 2026 09:27
- 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>
@jakesanders jakesanders force-pushed the handle_jsonb_date_ranges branch from 37f70f9 to a7b0a17 Compare June 8, 2026 16:32
@github-actions github-actions Bot added the repo label Jun 8, 2026
Comment thread .github/workflows/release.yaml Fixed
Comment thread .github/workflows/release.yaml Fixed
Comment thread lib/sift/where_handler.rb
end

def date_range?(val)
val.is_a?(String) && val.include?("...")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

@mattl3w1s mattl3w1s left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Agent review

Comment thread lib/sift/where_handler.rb
"#{@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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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?("...") is true, so parse_as_range? fires and calls value.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 a Range from the first two ...-separated JSON fragments. That Range reaches apply_jsonb_conditions, which iterates it via String#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.

Comment thread lib/sift/where_handler.rb
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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).

Comment thread lib/sift/where_handler.rb
Comment on lines +29 to +31
elsif date_range?(val)
from_date, end_date = normalized_date_range(val)
collection.where("#{@param.internal_name}->>'#{key}' BETWEEN ? AND ?", from_date, end_date)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 }, {}, [])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Good suggestion

====

[warning] testing

This test calls handler.call directly with a pre-parsed Hash, bypassing Filter#parameterizeValueParser. 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants