Skip to content

Fix _present / _blank producing always-UNKNOWN clause on non-string columns (#1552)#1678

Open
ryoya1122 wants to merge 1 commit into
activerecord-hackery:v5.0.0from
ryoya1122:fix/issue-1552-present-blank-non-string
Open

Fix _present / _blank producing always-UNKNOWN clause on non-string columns (#1552)#1678
ryoya1122 wants to merge 1 commit into
activerecord-hackery:v5.0.0from
ryoya1122:fix/issue-1552-present-blank-non-string

Conversation

@ryoya1122

Copy link
Copy Markdown
Contributor

Closes #1552.

Problem

The built-in _present (and mirrored _blank) predicate produces broken SQL on every non-string column:

Product.ransack(stock_present: true).result.to_sql
# => SELECT "products".*
#    FROM "products"
#    WHERE ("products"."stock" IS NOT NULL
#       AND "products"."stock" != NULL)     ← always UNKNOWN

The trailing != NULL is never true, so stock_present: true returns 0 rows even when every row has a non-null stock. The reporter in #1552 hit the same shape on a Postgres string column with a unique partial index, but it reproduces just as cleanly on any boolean / integer / decimal / datetime column.

String / text columns are unaffected — they correctly produce the documented IS NOT NULL AND != '' form.

Cause

The present / blank predicate definitions in lib/ransack/constants.rb ship a column-type-agnostic formatter:

formatter: proc { |v| [nil, ''.freeze].freeze }

Condition#format_predicate feeds those values into Arel's NOT_EQ_ALL / EQ_ANY. For non-string columns, ActiveRecord casts '' to the column type, which is NULL, leaving the always-UNKNOWN comparison.

Fix

Condition#format_predicate now strips the empty-string entry from arel_values when the attribute is not a string-like column (:string, :text, :citext). The two-clause form is preserved on string / text columns; non-string columns reduce to IS NOT NULL (or IS NULL for _blank) — which matches what Object#present? means for those types in Rails.

# After:
Product.ransack(stock_present: true).result.to_sql
# => SELECT "products".* FROM "products" WHERE ("products"."stock" IS NOT NULL)

Compatibility

Column type Predicate Before After
:string / :text / :citext _present (true) IS NOT NULL AND != '' IS NOT NULL AND != '' ✓ unchanged
:string / :text / :citext _blank (true) IS NULL OR = '' IS NULL OR = '' ✓ unchanged
:integer / :decimal / :boolean / :datetime / etc. _present (true) IS NOT NULL AND != NULL IS NOT NULL ✓ fixed
:integer / :decimal / :boolean / :datetime / etc. _blank (true) IS NULL OR = NULL IS NULL ✓ fixed
unknown / nil type (e.g. ransackers without explicit type) _present / _blank two-clause form two-clause form ✓ unchanged (treated as string-like, conservative default)

The condition for stripping is intentionally narrow:

if arel_values.is_a?(Array) && arel_values.include?('') && !string_like_attribute?(attribute)
  arel_values = arel_values.reject { |v| v == '' }
end
  • Only fires when the formatter actually emitted '' — custom predicates that don't use that envelope are untouched.
  • Only fires when the attribute is provably non-string-like — unknown types keep the existing behaviour.

Tests

Four new specs in spec/ransack/predicate_spec.rb, beside the existing string-column ones:

  • _present / _blank on salary (integer) emits only IS [NOT] NULL, never != NULL or = NULL
  • existing name (string) tests continue to pass, verifying the two-clause form is preserved

Full suite: 516 examples, 0 failures, 1 pending on v5.0.0 + this branch (SQLite, ActiveRecord 7.2.3.1, Ruby 3.4.9).

Targets v5.0.0 per #1640.

…ns (activerecord-hackery#1552)

The `present` and `blank` predicates ship a formatter that returns
`[nil, '']` regardless of column type. For string-like columns this
turns into the documented `column IS NOT NULL AND column != ''` (and
the mirrored OR for `blank`). For non-string columns however
ActiveRecord casts the empty string to the column type, which yields
`NULL`, leaving the always-UNKNOWN `column != NULL` clause:

    Product.ransack(stock_present: true).result.to_sql
    # => SELECT "products".* FROM "products"
    #    WHERE ("products"."stock" IS NOT NULL
    #       AND "products"."stock" != NULL)

The second half never matches, so `stock_present: true` returned 0
rows even when every row had a non-null stock.

`Condition#format_predicate` now strips the empty string from
`arel_values` when the attribute is not string-like (`:string`,
`:text`, `:citext`). String/text columns keep the documented two-clause
form; non-string columns reduce to `IS NOT NULL` / `IS NULL` only,
which is what `Object#present?` means for those types in Rails.
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