module Ransack
module Constants
OR = 'or'.freeze
AND = 'and'.freeze
CAP_SEARCH = 'Search'.freeze
SEARCH = 'search'.freeze
SEARCHES = 'searches'.freeze
ATTRIBUTE = 'attribute'.freeze
ATTRIBUTES = 'attributes'.freeze
COMBINATOR = 'combinator'.freeze
TWO_COLONS = '::'.freeze
UNDERSCORE = '_'.freeze
LEFT_PARENTHESIS = '('.freeze
Q = 'q'.freeze
I = 'i'.freeze
DOT_ASTERIX = '.*'.freeze
STRING_JOIN = 'string_join'.freeze
ASSOCIATION_JOIN = 'association_join'.freeze
STASHED_JOIN = 'stashed_join'.freeze
JOIN_NODE = 'join_node'.freeze
TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE'].to_set
FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE'].to_set
BOOLEAN_VALUES = (TRUE_VALUES + FALSE_VALUES).freeze
AND_OR = ['and'.freeze, 'or'.freeze].freeze
IN_NOT_IN = ['in'.freeze, 'not_in'.freeze].freeze
SUFFIXES = ['_any'.freeze, '_all'.freeze].freeze
AREL_PREDICATES = [
'eq'.freeze, 'not_eq'.freeze,
'matches'.freeze, 'does_not_match'.freeze,
'lt'.freeze, 'lteq'.freeze,
'gt'.freeze, 'gteq'.freeze,
'in'.freeze, 'not_in'.freeze
].freeze
A_S_I = ['a'.freeze, 's'.freeze, 'i'.freeze].freeze
# The predicates `Ransack.options[:null_sentinel]` applies to. Limited to
# the positive equality family (`eq`, `in`, and their `_any` compounds)
# because these are the only predicates where "or is null" is
# unambiguous. The `_all` compounds are excluded because they are
# conjunctive: `eq_all`/`in_all` require a single column to equal every
# supplied value at once, so ORing in `IS NULL` would widen a query that
# already matches nothing (or almost nothing) rather than narrow it. A
# negative predicate treats the sentinel as a literal value for the same
# reason of unambiguity: `not_eq` matching the sentinel would otherwise
# mean "is not unassigned", which reads as the opposite of what a caller
# submitting it alone almost certainly wants.
NULL_SENTINEL_PREDICATES = [
'eq'.freeze, 'in'.freeze, 'eq_any'.freeze, 'in_any'.freeze
].freeze
EQ = 'eq'.freeze
NOT_EQ = 'not_eq'.freeze
EQ_ANY = 'eq_any'.freeze
NOT_EQ_ALL = 'not_eq_all'.freeze
CONT = 'cont'.freeze
RANSACK_SLASH_SEARCHES = 'ransack/searches'.freeze
RANSACK_SLASH_SEARCHES_SLASH_SEARCH = 'ransack/searches/search'.freeze
DISTINCT = 'DISTINCT '.freeze
# The most `_`-separated segments a single condition or sort key may have
# before Ransack rejects it as invalid instead of parsing it. Working out
# whether a key is an attribute, an `_and_`/`_or_` compound or a path
# through associations is superlinear in this count, and the key comes
# straight from the query string, so an unbounded one is a CPU-exhaustion
# denial of service (GHSA-j3f8-w227-4hh8). No real search key is this
# long: OR-ing every column of a very wide table stays well under it.
MAX_KEY_DEPTH = 200
DERIVED_PREDICATES = [
[CONT, {
arel_predicate: 'matches'.freeze,
formatter: proc { |v| "%#{escape_wildcards(v)}%" }
}
],
['not_cont'.freeze, {
arel_predicate: 'does_not_match'.freeze,
formatter: proc { |v| "%#{escape_wildcards(v)}%" }
}
],
['i_cont'.freeze, {
arel_predicate: 'matches'.freeze,
formatter: proc { |v| "%#{escape_wildcards(v.downcase)}%" },
case_insensitive: true
}
],
['not_i_cont'.freeze, {
arel_predicate: 'does_not_match'.freeze,
formatter: proc { |v| "%#{escape_wildcards(v.downcase)}%" },
case_insensitive: true
}
],
['start'.freeze, {
arel_predicate: 'matches'.freeze,
formatter: proc { |v| "#{escape_wildcards(v)}%" }
}
],
['not_start'.freeze, {
arel_predicate: 'does_not_match'.freeze,
formatter: proc { |v| "#{escape_wildcards(v)}%" }
}
],
['i_start'.freeze, {
arel_predicate: 'matches'.freeze,
formatter: proc { |v| "#{escape_wildcards(v.downcase)}%" },
case_insensitive: true
}
],
['not_i_start'.freeze, {
arel_predicate: 'does_not_match'.freeze,
formatter: proc { |v| "#{escape_wildcards(v.downcase)}%" },
case_insensitive: true
}
],
['end'.freeze, {
arel_predicate: 'matches'.freeze,
formatter: proc { |v| "%#{escape_wildcards(v)}" }
}
],
['not_end'.freeze, {
arel_predicate: 'does_not_match'.freeze,
formatter: proc { |v| "%#{escape_wildcards(v)}" }
}
],
['i_end'.freeze, {
arel_predicate: 'matches'.freeze,
formatter: proc { |v| "%#{escape_wildcards(v.downcase)}" },
case_insensitive: true
}
],
['not_i_end'.freeze, {
arel_predicate: 'does_not_match'.freeze,
formatter: proc { |v| "%#{escape_wildcards(v.downcase)}" },
case_insensitive: true
}
],
['true'.freeze, {
arel_predicate: proc { |v| v ? EQ : NOT_EQ },
compounds: false,
type: :boolean,
validator: proc { |v| BOOLEAN_VALUES.include?(v) },
formatter: proc { |v| true }
}
],
['not_true'.freeze, {
arel_predicate: proc { |v| v ? NOT_EQ : EQ },
compounds: false,
type: :boolean,
validator: proc { |v| BOOLEAN_VALUES.include?(v) },
formatter: proc { |v| true }
}
],
['false'.freeze, {
arel_predicate: proc { |v| v ? EQ : NOT_EQ },
compounds: false,
type: :boolean,
validator: proc { |v| BOOLEAN_VALUES.include?(v) },
formatter: proc { |v| false }
}
],
['not_false'.freeze, {
arel_predicate: proc { |v| v ? NOT_EQ : EQ },
compounds: false,
type: :boolean,
validator: proc { |v| BOOLEAN_VALUES.include?(v) },
formatter: proc { |v| false }
}
],
['present'.freeze, {
arel_predicate: proc { |v| v ? NOT_EQ_ALL : EQ_ANY },
compounds: false,
type: :boolean,
validator: proc { |v| BOOLEAN_VALUES.include?(v) },
formatter: proc { |v| [nil, ''.freeze].freeze }
}
],
['blank'.freeze, {
arel_predicate: proc { |v| v ? EQ_ANY : NOT_EQ_ALL },
compounds: false,
type: :boolean,
validator: proc { |v| BOOLEAN_VALUES.include?(v) },
formatter: proc { |v| [nil, ''.freeze].freeze }
}
],
['null'.freeze, {
arel_predicate: proc { |v| v ? EQ : NOT_EQ },
compounds: false,
type: :boolean,
validator: proc { |v| BOOLEAN_VALUES.include?(v) },
formatter: proc { |v| nil }
}
],
['not_null'.freeze, {
arel_predicate: proc { |v| v ? NOT_EQ : EQ },
compounds: false,
type: :boolean,
validator: proc { |v| BOOLEAN_VALUES.include?(v) },
formatter: proc { |v| nil } }
],
['length_eq'.freeze, {
arel_predicate: 'eq'.freeze,
type: :integer
}
],
['length_lt'.freeze, {
arel_predicate: 'lt'.freeze,
type: :integer
}
],
['length_lteq'.freeze, {
arel_predicate: 'lteq'.freeze,
type: :integer
}
],
['length_gt'.freeze, {
arel_predicate: 'gt'.freeze,
type: :integer
}
],
['length_gteq'.freeze, {
arel_predicate: 'gteq'.freeze,
type: :integer
}
]
].freeze
# The character used to escape LIKE wildcards. Emitted as an explicit
# `ESCAPE` clause alongside every LIKE / NOT LIKE predicate, so the
# behaviour does not depend on a backend's default escape character —
# MySQL and PostgreSQL default to backslash, SQLite has no default at all.
LIKE_ESCAPE_CHARACTER = '\\'.freeze
module_function
# Escapes the LIKE wildcards `%` and `_`, and the escape character itself,
# so they are matched literally. Paired with the `ESCAPE` clause added in
# `Condition#format_predicate`; escaping without it silently does nothing
# on SQLite. See https://github.com/activerecord-hackery/ransack/issues/1581
def escape_wildcards(unescaped)
unescaped.to_s.gsub(/([\\%_])/) { "#{LIKE_ESCAPE_CHARACTER}#{$1}" }
end
def null_sentinel_predicate?(predicate_name, sentinel)
sentinel && NULL_SENTINEL_PREDICATES.include?(predicate_name)
end
def null_sentinel_value?(value, sentinel)
sentinel && value == sentinel
end
def null_sentinel_requested?(predicate_name, values, sentinel)
null_sentinel_predicate?(predicate_name, sentinel) &&
values.any? { |v| null_sentinel_value?(v.value, sentinel) }
end
end
end