babik 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +16 -0
  3. data/README.md +718 -0
  4. data/Rakefile +18 -0
  5. data/lib/babik.rb +122 -0
  6. data/lib/babik/database.rb +16 -0
  7. data/lib/babik/queryset.rb +154 -0
  8. data/lib/babik/queryset/components/aggregation.rb +172 -0
  9. data/lib/babik/queryset/components/limit.rb +22 -0
  10. data/lib/babik/queryset/components/order.rb +161 -0
  11. data/lib/babik/queryset/components/projection.rb +118 -0
  12. data/lib/babik/queryset/components/select_related.rb +78 -0
  13. data/lib/babik/queryset/components/sql_renderer.rb +99 -0
  14. data/lib/babik/queryset/components/where.rb +43 -0
  15. data/lib/babik/queryset/lib/association/foreign_association_chain.rb +97 -0
  16. data/lib/babik/queryset/lib/association/select_related_association_chain.rb +32 -0
  17. data/lib/babik/queryset/lib/condition.rb +103 -0
  18. data/lib/babik/queryset/lib/field.rb +34 -0
  19. data/lib/babik/queryset/lib/join/association_joiner.rb +39 -0
  20. data/lib/babik/queryset/lib/join/join.rb +86 -0
  21. data/lib/babik/queryset/lib/selection/config.rb +19 -0
  22. data/lib/babik/queryset/lib/selection/foreign_selection.rb +39 -0
  23. data/lib/babik/queryset/lib/selection/local_selection.rb +40 -0
  24. data/lib/babik/queryset/lib/selection/operation/base.rb +126 -0
  25. data/lib/babik/queryset/lib/selection/operation/date.rb +178 -0
  26. data/lib/babik/queryset/lib/selection/operation/operations.rb +201 -0
  27. data/lib/babik/queryset/lib/selection/operation/regex.rb +58 -0
  28. data/lib/babik/queryset/lib/selection/path/foreign_path.rb +50 -0
  29. data/lib/babik/queryset/lib/selection/path/local_path.rb +44 -0
  30. data/lib/babik/queryset/lib/selection/path/path.rb +23 -0
  31. data/lib/babik/queryset/lib/selection/select_related_selection.rb +38 -0
  32. data/lib/babik/queryset/lib/selection/selection.rb +19 -0
  33. data/lib/babik/queryset/lib/update/assignment.rb +108 -0
  34. data/lib/babik/queryset/mixins/aggregatable.rb +17 -0
  35. data/lib/babik/queryset/mixins/bounded.rb +38 -0
  36. data/lib/babik/queryset/mixins/clonable.rb +52 -0
  37. data/lib/babik/queryset/mixins/countable.rb +44 -0
  38. data/lib/babik/queryset/mixins/deletable.rb +13 -0
  39. data/lib/babik/queryset/mixins/distinguishable.rb +27 -0
  40. data/lib/babik/queryset/mixins/filterable.rb +51 -0
  41. data/lib/babik/queryset/mixins/limitable.rb +88 -0
  42. data/lib/babik/queryset/mixins/lockable.rb +31 -0
  43. data/lib/babik/queryset/mixins/none.rb +16 -0
  44. data/lib/babik/queryset/mixins/projectable.rb +34 -0
  45. data/lib/babik/queryset/mixins/related_selector.rb +28 -0
  46. data/lib/babik/queryset/mixins/set_operations.rb +32 -0
  47. data/lib/babik/queryset/mixins/sortable.rb +49 -0
  48. data/lib/babik/queryset/mixins/sql_renderizable.rb +17 -0
  49. data/lib/babik/queryset/mixins/updatable.rb +14 -0
  50. data/lib/babik/queryset/templates/default/delete/main.sql.erb +14 -0
  51. data/lib/babik/queryset/templates/default/select/components/aggregation.sql.erb +5 -0
  52. data/lib/babik/queryset/templates/default/select/components/from.sql.erb +16 -0
  53. data/lib/babik/queryset/templates/default/select/components/from_set.sql.erb +3 -0
  54. data/lib/babik/queryset/templates/default/select/components/from_table.sql.erb +2 -0
  55. data/lib/babik/queryset/templates/default/select/components/limit.sql.erb +10 -0
  56. data/lib/babik/queryset/templates/default/select/components/order_by.sql.erb +9 -0
  57. data/lib/babik/queryset/templates/default/select/components/projection.sql.erb +7 -0
  58. data/lib/babik/queryset/templates/default/select/components/select_related.sql.erb +26 -0
  59. data/lib/babik/queryset/templates/default/select/components/where.sql.erb +39 -0
  60. data/lib/babik/queryset/templates/default/select/main.sql.erb +42 -0
  61. data/lib/babik/queryset/templates/default/update/main.sql.erb +15 -0
  62. data/lib/babik/queryset/templates/mssql/select/components/limit.sql.erb +8 -0
  63. data/lib/babik/queryset/templates/mssql/select/components/order_by.sql.erb +21 -0
  64. data/lib/babik/queryset/templates/mysql2/delete/main.sql.erb +15 -0
  65. data/lib/babik/queryset/templates/mysql2/update/main.sql.erb +18 -0
  66. data/lib/babik/queryset/templates/sqlite3/select/components/from_set.sql.erb +5 -0
  67. data/test/config/db/schema.rb +83 -0
  68. data/test/config/models/bad_post.rb +5 -0
  69. data/test/config/models/bad_tag.rb +5 -0
  70. data/test/config/models/category.rb +4 -0
  71. data/test/config/models/geozone.rb +6 -0
  72. data/test/config/models/group.rb +5 -0
  73. data/test/config/models/group_user.rb +5 -0
  74. data/test/config/models/post.rb +24 -0
  75. data/test/config/models/post_tag.rb +5 -0
  76. data/test/config/models/tag.rb +5 -0
  77. data/test/config/models/user.rb +6 -0
  78. data/test/delete/delete_test.rb +60 -0
  79. data/test/delete/foreign_conditions_delete_test.rb +57 -0
  80. data/test/delete/local_conditions_delete_test.rb +20 -0
  81. data/test/enable_coverage.rb +17 -0
  82. data/test/lib/selection/operation/log/test-queries.log +1 -0
  83. data/test/lib/selection/operation/test_date.rb +131 -0
  84. data/test/lib/selection/operation/test_regex.rb +55 -0
  85. data/test/other/clone_test.rb +129 -0
  86. data/test/other/escape_test.rb +21 -0
  87. data/test/other/inverse_of_required_test.rb +33 -0
  88. data/test/select/aggregate_test.rb +151 -0
  89. data/test/select/bounds_test.rb +46 -0
  90. data/test/select/count_test.rb +147 -0
  91. data/test/select/distinct_test.rb +38 -0
  92. data/test/select/exclude_test.rb +72 -0
  93. data/test/select/filter_from_object_test.rb +125 -0
  94. data/test/select/filter_test.rb +207 -0
  95. data/test/select/for_update_test.rb +19 -0
  96. data/test/select/foreign_selection_test.rb +60 -0
  97. data/test/select/get_test.rb +40 -0
  98. data/test/select/limit_test.rb +109 -0
  99. data/test/select/local_selection_test.rb +24 -0
  100. data/test/select/lookup_test.rb +208 -0
  101. data/test/select/none_test.rb +40 -0
  102. data/test/select/order_test.rb +165 -0
  103. data/test/select/project_test.rb +107 -0
  104. data/test/select/select_related_test.rb +124 -0
  105. data/test/select/subquery_test.rb +50 -0
  106. data/test/set_operations/basic_usage_test.rb +121 -0
  107. data/test/test_helper.rb +55 -0
  108. data/test/update/update_test.rb +93 -0
  109. metadata +278 -0
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Common module for Babik library
4
+ module Babik
5
+ # QuerySet module
6
+ module QuerySet
7
+ # Where conditions
8
+ class Where
9
+
10
+ attr_reader :model, :inclusion_filters, :exclusion_filters
11
+
12
+ def initialize(model)
13
+ @model = model
14
+ @inclusion_filters = []
15
+ @exclusion_filters = []
16
+ end
17
+
18
+ def exclusion_filters?
19
+ @exclusion_filters.length.positive?
20
+ end
21
+
22
+ def inclusion_filters?
23
+ @inclusion_filters.length.positive?
24
+ end
25
+
26
+ def add_exclusion_filter(filter)
27
+ @exclusion_filters << Babik::QuerySet::Condition.factory(@model, filter)
28
+ end
29
+
30
+ def add_inclusion_filter(filter)
31
+ @inclusion_filters << Babik::QuerySet::Condition.factory(@model, filter)
32
+ end
33
+
34
+ def left_joins_by_alias
35
+ left_joins_by_alias = {}
36
+ [@inclusion_filters, @exclusion_filters].flatten.each do |filter|
37
+ left_joins_by_alias.merge!(filter.left_joins_by_alias)
38
+ end
39
+ left_joins_by_alias
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Babik
4
+ module Association
5
+
6
+ # Association chain for association paths
7
+ # An association chain is a chain of associations
8
+ # where the target model of association i is the origin model of association i + 1
9
+ # Remember, an association path is of the form: zone::parent_zone, category::posts::tags
10
+ class ForeignAssociationChain
11
+
12
+ attr_reader :model, :associations, :target_model, :selection_path
13
+
14
+ # Construct the association chain
15
+ # @param model [ActiveRecord::Base] origin model
16
+ # @param association_path [Array] association path as an array.
17
+ # @param selection_path [String, Symbol] selection path used only to raise errors.
18
+ def initialize(model, association_path, selection_path)
19
+ @model = model
20
+ @association_path = association_path
21
+ @selection_path = selection_path
22
+ _init_associations
23
+ end
24
+
25
+ # Init associations
26
+ def _init_associations
27
+ @associations = []
28
+ associated_model_i = @model
29
+ @association_path.each do |association_i_name|
30
+ associated_model_i = _init_association(associated_model_i, association_i_name)
31
+ end
32
+ @target_model = associated_model_i
33
+ end
34
+
35
+ # Initialize association by name
36
+ # @param model [ActiveRecord::Base] origin model of the association association_name
37
+ # @param association_name [String, Symbol] association name.
38
+ # @return [ActiveRecord::Base] target model of ith association.
39
+ def _init_association(model, association_name)
40
+ association = _assert_association(model, association_name)
41
+ _association_pass(association)
42
+ end
43
+
44
+ # Each one of the asssociation
45
+ # @param association_i [AssociationReflection] ith association.
46
+ # @return [ActiveRecord::Base] target model of ith association.
47
+ def _association_pass(association_i)
48
+ # To one relationship
49
+ if association_i.belongs_to? || association_i.has_one?
50
+ @associations << association_i
51
+ return association_i.klass
52
+ end
53
+
54
+ # Many-to-many with through relationship
55
+ # The has-and-belongs-to-many relationships have been detected and filtered in _construct_association
56
+
57
+ # Add model-through association (active_record -> klass)
58
+ if association_i.through_reflection
59
+ @associations << association_i.through_reflection
60
+ # Add through-target association (through -> target)
61
+ target_name = association_i.source_reflection_name
62
+ through_model = association_i.through_reflection.klass
63
+ through_target_association = through_model.reflect_on_association(target_name)
64
+ @associations << through_target_association
65
+ # The next association comes from target model
66
+ return through_target_association.klass
67
+ end
68
+
69
+ # Add direct has_many association
70
+ @associations << association_i
71
+ Object.const_get(association_i.class_name)
72
+ end
73
+
74
+ # Return an association or raise an exception if is not an allowed association
75
+ # @return [Association] Association of model
76
+ def _assert_association(association_model, association_name)
77
+ association = association_model.reflect_on_association(association_name.to_sym)
78
+
79
+ # Check the association exists
80
+ unless association
81
+ raise "Bad selection path: #{@selection_path}. #{association_name} not found "\
82
+ "in model #{association_model} when filtering #{@model} objects"
83
+ end
84
+
85
+ # Check the association is no a has-and belongs-to-many
86
+ # These associations are discouraged by Rails Community
87
+ if association.class == ActiveRecord::Reflection::HasAndBelongsToManyReflection
88
+ raise "Relationship #{association.name} is has_and_belongs_to_many. Convert it to has_many-through"
89
+ end
90
+
91
+ # Valid association
92
+ association
93
+ end
94
+ end
95
+
96
+ end
97
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'babik/queryset/lib/association/foreign_association_chain'
4
+
5
+ module Babik
6
+ module Association
7
+
8
+ # Association chain for association paths
9
+ # An association chain is a chain of associations
10
+ # where the target model of association i is the origin model of association i + 1
11
+ # Remember, an association path is of the form: zone::parent_zone, category::posts::tags
12
+ class SelectRelatedAssociationChain < ForeignAssociationChain
13
+
14
+ # Each one of the association
15
+ # @param association_i [AssociationReflection] ith association.
16
+ # @return [ActiveRecord::Base] target model of ith association.
17
+ def _association_pass(association_i)
18
+ # To one relationship
19
+ if association_i.belongs_to? || association_i.has_one?
20
+ @associations << association_i
21
+ associated_model_i = association_i.klass
22
+ @target_model = associated_model_i
23
+ return @target_model
24
+ end
25
+ raise "Bad association path: #{association_i.name} in model #{association_i.klass} " \
26
+ "is not belongs_to or has_one when constructing select_related for #{@model} objects"
27
+ end
28
+
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Babik
4
+ module QuerySet
5
+ # Each one of the conditions that can appear in a SQL WHERE.
6
+ module Condition
7
+
8
+ # Return the Disjunction or Conjunction according to what class the filters parameter is.
9
+ # @param model [ActiveRecord::Base] Model owner of this condition.
10
+ # @param filter [Array, Hash] if it is an Array, it would be a disjunction.
11
+ # If a Hash, it would be a conjunction.
12
+ # @raise [RuntimeError] if the class of filters is not an Array or a Hash.
13
+ def self.factory(model, filter)
14
+ if filter.class == Array
15
+ return Disjunction.new(model, filter.map { |filter_i| Conjunction.new(model, filter_i) })
16
+ end
17
+ if filter.class == Hash
18
+ return Conjunction.new(model, filter)
19
+ end
20
+ raise '`filter\' parameter must be an Array for OR-based AND-conditions or a hash for a lone AND-condition'
21
+ end
22
+
23
+ # AND-based condition, also known as conjunction
24
+ class Conjunction
25
+ attr_reader :model, :selections
26
+
27
+ # Construct a conjunction condition.
28
+ # @param model [ActiveRecord::Base] Model owner of this condition.
29
+ # @param filter [Hash] a hash where the key identify field paths and the values the values they must take.
30
+ def initialize(model, filter)
31
+ @model = model
32
+ @selections = []
33
+ # filter is a Hash composed by :selection_path => value
34
+ filter.each do |selection_path, value|
35
+ @selections << Babik::Selection::Base.factory(@model, selection_path, value)
36
+ end
37
+ end
38
+
39
+ # Return a hash with the joins grouped by alias
40
+ # @return [Hash] alias: SQL::Join object
41
+ def left_joins_by_alias
42
+ left_joins_by_alias_ = {}
43
+ @selections.each do |selection|
44
+ left_joins_by_alias_.merge!(selection.left_joins_by_alias)
45
+ end
46
+ left_joins_by_alias_
47
+ end
48
+
49
+ # Return SQL code for this conjunction.
50
+ # e.g
51
+ # (first_name = 'Julius' AND last_name = 'Caesar' AND zone = 'Rome')
52
+ # @return [String] SQL code that will be used in the WHERE part of SQL SELECT statements.
53
+ def sql
54
+ @selections.map(&:sql_where_condition).join(" AND\n")
55
+ end
56
+ end
57
+
58
+ end
59
+
60
+ # Disjunction in Disjunctive Normal Form
61
+ # i.e OR-based condition of AND-based conditions (disjunction of conjunctions)
62
+ #
63
+ # See https://en.wikipedia.org/wiki/Disjunctive_normal_form
64
+ #
65
+ # e.g.
66
+ # (users.filter_name = 'Julius' AND posts.title = 'Stabbed to death: My story') OR
67
+ # (users.filter_name = 'Marcus Antonius' AND posts.title = 'A sword in my belly button')
68
+ #
69
+ class Disjunction
70
+ attr_reader :model, :conjunctions
71
+
72
+ # Construct a conjunction condition.
73
+ # @param model [ActiveRecord::Base] Model owner of this condition.
74
+ # @param conjunctions [Array] array of conjunctions that will be
75
+ # joined in a disjunction (hence the name Disjunctive Normal Form).
76
+ def initialize(model, conjunctions)
77
+ @model = model
78
+ @conjunctions = conjunctions
79
+ end
80
+
81
+ # Return a hash with the joins grouped by alias
82
+ # @return [Hash] alias: SQL::Join object
83
+ def left_joins_by_alias
84
+ left_joins_by_alias_ = {}
85
+ @conjunctions.each do |conjunction|
86
+ left_joins_by_alias_.merge!(conjunction.left_joins_by_alias)
87
+ end
88
+ left_joins_by_alias_
89
+ end
90
+
91
+ # Return SQL code for this disjunction.
92
+ # e.g
93
+ # (first_name = 'Julius' AND last_name = 'Caesar') OR (zone.name = 'Rome')
94
+ # @return [String] SQL code that will be used in the WHERE part of SQL SELECT statements.
95
+ def sql
96
+ "(\n#{@conjunctions.map(&:sql).join(" OR\n")}\n)"
97
+ end
98
+ end
99
+
100
+ end
101
+ end
102
+
103
+
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Common module for Babik library
4
+ module Babik
5
+ module Table
6
+ # Field module
7
+ # abstracts the concept of table field according to some useful conversions
8
+ class Field
9
+
10
+ # Create an actual field for a model.
11
+ # @param model [ActiveRecord::Base] model this field belongs to.
12
+ # @param field [String] field model that could need the conversion.
13
+ def initialize(model, field)
14
+ @model = model
15
+ @field = field
16
+ end
17
+
18
+ # Check if the field requires some conversion and if that's the case, return the converted final field
19
+ # If the field is a name of an association, it will be converted to the foreign entity id
20
+ # @return [String] Actual name of the field that will be used in the SQL.
21
+ def real_field
22
+ # If the selected field is a local attribute return the condition as-is (that's the most usual case)
23
+ is_local_attribute = @model.column_names.include?(@field.to_s)
24
+ return @field if is_local_attribute
25
+ # If the selected field is the name of an association, convert it to be a right condition
26
+ association = @model.reflect_on_association(@field.to_sym)
27
+ # Only if the association is belongs to, the other associations will be checked by foreign filter method
28
+ return association.foreign_key if association && association.belongs_to?
29
+ # Field that is not present in the model
30
+ raise "Unrecognized field #{@field} for model #{@model} in filter/exclude"
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Babik
4
+ module QuerySet
5
+ # Join between two tables
6
+ module Join
7
+ # Class whose mission is to catch an association path and construct all the sequence of LEFT JOINS
8
+ # that lies behind this set of associations.
9
+ class AssociationJoiner
10
+ attr_reader :left_joins_by_alias, :target_alias
11
+
12
+ # Read an Array of associations an construct a list of joins.
13
+ # @param associations [Array<ActiveRecord::Associations::Association>] Array of associations.
14
+ def initialize(associations)
15
+ @left_joins_by_alias = {}
16
+ last_table_alias = nil
17
+ associations.each_with_index do |association, association_path_index|
18
+ # Important, this variable will take the last joined table to chain the join, in the first loop, will take
19
+ # the association origin table name.
20
+ last_table_alias ||= association.active_record.table_name
21
+ left_join = Babik::QuerySet::Join.new_from_association(association, association_path_index, last_table_alias)
22
+
23
+ @left_joins_by_alias[left_join.target_alias] = left_join
24
+ last_table_alias = left_join.target_alias
25
+ end
26
+ @target_alias = last_table_alias
27
+ end
28
+
29
+ # Table alias will be another way of calling target alias,
30
+ # i.e. the alias of the target table in the join,
31
+ # i.e. the alias of the last table in the join,
32
+ # @return [String] Target table alias
33
+ def table_alias
34
+ @target_alias
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Babik
4
+ module QuerySet
5
+ # Join between two tables
6
+ module Join
7
+
8
+ # Construct a new Join from an association
9
+ # @param association Association between two ActiveRecord::Base objects.
10
+ # @param association_position Association position. Used when the relationship is a many-to-many through.
11
+ # @param origin_table_alias Alias of table that is the origin of the join.
12
+ # @param join [LeftJoin] Join class.
13
+ # @return [LeftJoin] object with the join for this association.
14
+ def self.new_from_association(association, association_position, origin_table_alias, join = LeftJoin)
15
+ owner_table = association.active_record.table_name
16
+ target_table_alias = "#{owner_table}__#{association.name}_#{association_position}"
17
+ join_keys = association.join_keys
18
+
19
+ target_table = TargetTable.new(association.table_name, target_table_alias, join_keys.key)
20
+ origin_table = OriginTable.new(origin_table_alias, join_keys.foreign_key)
21
+
22
+ join.new(target_table, origin_table)
23
+ end
24
+
25
+ # A table join
26
+ class AbstractJoin
27
+ attr_reader :target_table, :origin_table, :sql
28
+
29
+ # Construct a Join
30
+ # @param target_table [Babik::QuerySet::Join::TargetTable] target table of the join.
31
+ # @param origin_table [Babik::QuerySet::Join::OriginTable] origin table of the join.
32
+ def initialize(target_table, origin_table)
33
+ @target_table = target_table
34
+ @origin_table = origin_table
35
+ _init_sql
36
+ end
37
+
38
+ # Initialize SQL of the JOIN
39
+ def _init_sql
40
+ # Create the SQL code of the join
41
+ @sql = %(
42
+ #{self.class::JOIN_TYPE} JOIN #{@target_table.name} #{@target_table.table_alias}
43
+ ON #{@target_table.table_alias}.#{@target_table.key} = #{@origin_table.table_alias}.#{@origin_table.key}
44
+ ).delete("\n").gsub(/\s{2,}/, ' ').strip
45
+ end
46
+
47
+ # Return the target table alias.
48
+ # @return [String] Target table alias.
49
+ def target_alias
50
+ @target_table.table_alias
51
+ end
52
+ end
53
+
54
+ # Left join between tables
55
+ class LeftJoin < AbstractJoin
56
+ JOIN_TYPE = 'LEFT'
57
+ end
58
+
59
+ # Target table of the join
60
+ class TargetTable
61
+ attr_reader :name, :table_alias, :key
62
+ # Constructor
63
+ # @param name [String] target table name
64
+ # @param table_alias [String] target table alias
65
+ # @param key [String] field that serves as key in the target table.
66
+ def initialize(name, table_alias, key)
67
+ @name = name
68
+ @table_alias = table_alias
69
+ @key = key
70
+ end
71
+ end
72
+
73
+ # Origin table of the join
74
+ class OriginTable
75
+ attr_reader :table_alias, :key
76
+ # Constructor
77
+ # @param table_alias [String] origin table alias
78
+ # @param key [String] field that serves as key.
79
+ def initialize(table_alias, key)
80
+ @table_alias = table_alias
81
+ @key = key
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Babik
4
+ module Selection
5
+ # Selection configuration
6
+ class Config
7
+ # Relationship separator string
8
+ # e.g.
9
+ # - author::first_name__iregex where author is the association, first_name is the field and iregex the operator
10
+ # - posts::tags::name__contains where posts is related with tags and (tag) name is the field
11
+ # and contains the operator.
12
+ RELATIONSHIP_SEPARATOR = '::'
13
+
14
+ # Operator separator string
15
+ # e.g. first_name__iregex where first_name is the field an iregex the operator
16
+ OPERATOR_SEPARATOR = '__'
17
+ end
18
+ end
19
+ end