Class: TypedEAV::QueryBuilder
- Inherits:
-
Object
- Object
- TypedEAV::QueryBuilder
- Defined in:
- lib/typed_eav/query_builder.rb
Overview
Replaces the per-type Finder class hierarchy from active_fields.
Field owns operand casting and operator validation before this layer builds Arel. Active Record supplies bind plumbing for the already-normalized typed value; no manual SQL CAST() calls or per-type query caster classes are needed here.
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
-
.entity_ids(field, operator, value) ⇒ Object
Convenience: returns entity IDs matching the filter.
-
.filter(field, operator, value) ⇒ Object
Returns an ActiveRecord::Relation of TypedEAV::Value records matching the given field, operator, and comparison value.
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))
120 121 122 |
# File 'lib/typed_eav/query_builder.rb', line 120 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.
26 27 28 29 30 31 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 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 |
# File 'lib/typed_eav/query_builder.rb', line 26 def filter(field, operator, value) operator = operator.to_sym # Validate operator is supported by this field type. The gate runs # BEFORE column resolution so an unsupported operator raises a # descriptive ArgumentError instead of silently dispatching to # `operator_column`'s default (which would point at the wrong # column for multi-cell types). 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 # Route the operator to its physical column via the field-class # dispatch. Single-cell types return `value_columns.first` for every # operator — BC-safe. Multi-cell types (Currency) route operators # like `:eq` (amount) and `:currency_eq` (currency code) to # different columns. See `Field::TypedStorage.operator_column`. col = field.class.operator_column(operator) arel_col = values_table[col] base = value_scope(field) excluded = %i[is_null is_not_null contains not_contains starts_with ends_with] operand = field.cast_query_operand(operator, value) unless excluded.include?(operator) case operator when :eq, :currency_eq # :currency_eq (Phase 5 Currency) is semantically equality on the # routed column — Currency's operator_column override has already # routed `col` to :string_value, so reusing the eq_predicate is # the canonical implementation. Without this branch, the case # falls through to the `else` raise even though the column # dispatch resolved correctly. The operator-validation gate at # the top of #filter still narrows :currency_eq to Field::Currency # only — no other field type accepts it. eq_predicate(base, arel_col, col, operand) when :not_eq not_eq_predicate(base, arel_col, col, operand) when :references # Phase 5 Reference field. `value` may be an Integer FK OR an # AR record instance — `field.cast` normalizes both to an # integer FK (a class-mismatched record marks the cast invalid # via the second tuple element). Empty-relation semantics on # invalid cast: returning `base.where(col => nil)` would # collapse to :is_null which has different semantics ("rows # without an FK at all" rather than "rows referencing this # missing target"); `base.none` is the unambiguous "no match". # The :references operator is registered ONLY on Field::Reference # (the operator-validation gate above keeps it from leaking to # other types). if operand.nil? base.none else base.where(arel_col.eq(operand)) end when :gt base.where(arel_col.gt(operand)) when :gteq base.where(arel_col.gteq(operand)) when :lt base.where(arel_col.lt(operand)) when :lteq base.where(arel_col.lteq(operand)) when :between base.where(arel_col.between(operand)) 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} @> ?", [operand].to_json) when :all_eq # For json_value arrays: contains all given elements base.where("#{col} @> ?", operand.to_json) else raise ArgumentError, "Unhandled operator: #{operator}" end end |