Class: TypedEAV::QueryBuilder

Inherits:
Object
  • Object
show all
Defined in:
lib/typed_eav/query_builder.rb

Overview

Replaces the per-type Finder class hierarchy from active_fields.

Because values live in native typed columns, ActiveRecord already knows the column types from the schema. Arel predicates (eq, gt, lt, matches, etc.) automatically go through the column’s ActiveRecord::Type for casting.

This means:

where(integer_value: "42")  ->  Rails casts "42" to 42 automatically
arel[:date_value].gt(value) ->  Rails casts string dates to Date objects

No manual CAST() calls. No per-type caster classes for queries. One module handles all field types.

Usage:

QueryBuilder.filter(field, :gt, 42)
# => ActiveRecord::Relation scoped to matching values

QueryBuilder.filter(field, :contains, "hello")
# => ILIKE query against the field's string_value column

Class Method Summary collapse

Class Method Details

.entity_ids(field, operator, value) ⇒ Object

Convenience: returns entity IDs matching the filter. Useful for subqueries: Model.where(id: QueryBuilder.entity_ids(field, :gt, 5))



95
96
97
# File 'lib/typed_eav/query_builder.rb', line 95

def entity_ids(field, operator, value)
  filter(field, operator, value).distinct.select(:entity_id)
end

.filter(field, operator, value) ⇒ Object

Returns an ActiveRecord::Relation of TypedEAV::Value records matching the given field, operator, and comparison value.

The relation is suitable for subquery use:

Model.where(id: QueryBuilder.filter(field, :gt, 5).select(:entity_id))

rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength – one operator-dispatch case statement; flattening keeps the supported-operators list scannable in one place.



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/typed_eav/query_builder.rb', line 32

def filter(field, operator, value)
  col = field.class.value_column
  operator = operator.to_sym

  # Validate operator is supported by this field type
  supported = field.class.supported_operators
  unless supported.include?(operator)
    raise ArgumentError,
          "Operator :#{operator} is not supported for #{field.class.name}. " \
          "Supported operators: #{supported.map { |o| ":#{o}" }.join(", ")}"
  end

  arel_col = values_table[col]

  base = value_scope(field)

  case operator
  when :eq
    eq_predicate(base, arel_col, col, value)
  when :not_eq
    not_eq_predicate(base, arel_col, col, value)
  when :gt
    base.where(arel_col.gt(value))
  when :gteq
    base.where(arel_col.gteq(value))
  when :lt
    base.where(arel_col.lt(value))
  when :lteq
    base.where(arel_col.lteq(value))
  when :between
    unless value.respond_to?(:first) && value.respond_to?(:last)
      raise ArgumentError,
            ":between expects a Range or two-element Array"
    end

    base.where(arel_col.between(value.first..value.last))
  when :contains
    base.where(arel_col.matches("%#{sanitize_like(value)}%"))
  when :not_contains
    base.where(arel_col.does_not_match("%#{sanitize_like(value)}%"))
  when :starts_with
    base.where(arel_col.matches("#{sanitize_like(value)}%"))
  when :ends_with
    base.where(arel_col.matches("%#{sanitize_like(value)}"))
  when :is_null
    base.where(col => nil)
  when :is_not_null
    base.where.not(col => nil)
  when :any_eq
    # For json_value arrays: contains the given element
    base.where("#{col} @> ?", [value].to_json)
  when :all_eq
    # For json_value arrays: contains all given elements
    base.where("#{col} @> ?", Array(value).to_json)
  else
    raise ArgumentError, "Unhandled operator: #{operator}"
  end
end