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,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'babik/queryset/lib/selection/path/foreign_path'
4
+
5
+ module Babik
6
+ module Selection
7
+ # Foreign selection
8
+ class ForeignSelection < Babik::Selection::Path::ForeignPath
9
+
10
+ attr_reader :model, :selection_path, :selected_field,
11
+ :sql_where_condition,
12
+ :value, :operator
13
+
14
+ # Create a foreign selection, that is, a filter that is based on a foreign field condition.
15
+ # @param model [ActiveRecord::Base] Model
16
+ # @param selection_path [String, Symbol] selection path used only to raise errors. e.g.:
17
+ # posts::category__in
18
+ # author::posts::tags
19
+ # creation_at__date__gte
20
+ # @param value [String, Integer, ActiveRecord::Base] value that will be used in the filter
21
+ def initialize(model, selection_path, value)
22
+ super(model, selection_path)
23
+ # If the value is an ActiveRecord model, get its id
24
+ @value = value
25
+ @value = @value.id if @value.is_a?(ActiveRecord::Base)
26
+ _init_sql_where_condition
27
+ end
28
+
29
+ # Initialize the SQL condition that will be used on the SQL SELECT
30
+ def _init_sql_where_condition
31
+ last_association_model = @association_chain.target_model
32
+ actual_field = Babik::Table::Field.new(last_association_model, @selected_field).real_field
33
+ @sql_where_condition = Babik::Selection::Operation::Base.factory(
34
+ "#{target_alias}.#{actual_field}", @operator, @value
35
+ )
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'babik/queryset/lib/selection/selection'
4
+ require 'babik/queryset/lib/selection/operation/operations'
5
+ require 'babik/queryset/lib/selection/path/local_path'
6
+
7
+ module Babik
8
+ module Selection
9
+ # Selection by a local field
10
+ class LocalSelection < Babik::Selection::Path::LocalPath
11
+
12
+ attr_reader :model, :selection_path, :selected_field, :operator, :secondary_operator, :value
13
+
14
+ # Construct a local field selector
15
+ # @param model [ActiveRecord::Base] model whose field will be used.
16
+ # @param selection_path [String] selection path. Of the form <field>__<operator>. e.g. first_name__equal, stars__gt
17
+ # If no operator is given (first_name), 'equal' will be used.
18
+ # @param value [String,Integer,Float,ActiveRecord::Base,Babik::QuerySet::Base] anything that can be used
19
+ # to select objects.
20
+ def initialize(model, selection_path, value)
21
+ super(model, selection_path)
22
+ @value = value
23
+ end
24
+
25
+ # Return the SQL where condition
26
+ # @return [Babik::Selection::Operation::Base] Condition obtained from the selection path and value.
27
+ def sql_where_condition
28
+ actual_field = Babik::Table::Field.new(model, @selected_field).real_field
29
+ # Return the condition
30
+ operator = if @secondary_operator
31
+ [@operator, @secondary_operator]
32
+ else
33
+ @operator
34
+ end
35
+ Babik::Selection::Operation::Base.factory("#{self.target_alias}.#{actual_field}", operator, @value)
36
+ end
37
+
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Babik
4
+ module Selection
5
+ # SQL operation module
6
+ module Operation
7
+ # Base class
8
+ class Base
9
+
10
+ attr_reader :field, :value, :sql_operation, :sql_operation_template
11
+
12
+ # Construct a SQL operation
13
+ # @param field [String] Name of the field. Prefixed with the table or table alias.
14
+ # @param sql_operation [String] Template string with the SQL code or the operation.
15
+ # Something like ?field = ?value.
16
+ def initialize(field, sql_operation, value)
17
+ @field = field
18
+ @value = value
19
+ @sql_operation_template = sql_operation.dup
20
+ @sql_operation = sql_operation.dup
21
+ _init_sql_operation
22
+ end
23
+
24
+ # Replace the SQL operation template and store the result in sql_operation attribute
25
+ def _init_sql_operation
26
+ @sql_operation = @sql_operation_template.sub('?field', @field).sub('?value', '?')
27
+ # Use Rails SQL escaping and avoid possible SQL-injection issues
28
+ # and also several DB-related issues (like MySQL escaping quotes by \' instead of '')
29
+ # Only if it has not been already replaced
30
+ @sql_operation = ActiveRecord::Base.sanitize_sql([@sql_operation, @value]) if @sql_operation.include?('?')
31
+ end
32
+
33
+ # Convert the operation to string
34
+ # @return [String] Return the replaced SQL operation.
35
+ def to_s
36
+ @sql_operation
37
+ end
38
+
39
+ # Return the database engine: sqlite3, mysql, postgres, mssql, etc.
40
+ def db_engine
41
+ Babik::Database.config[:adapter]
42
+ end
43
+
44
+ # Operation factory
45
+ def self.factory(field, operator, value)
46
+ # Some operators can have a secondary operator, like the year lookup that can be followed by
47
+ # a gt, lt, equal, etc. Check this case, and get it to prepare its passing to the operation.
48
+ raw_main_operator, secondary_operator = self.initialize_operators(operator)
49
+ # The field, operator or value can change in some special cases, e.g. if operator is equals and the value
50
+ # is an array, the operator should be 'in' actually.
51
+ field, main_operator, value = self.special_cases(field, raw_main_operator, value)
52
+ # At last, initialize operation
53
+ self.initialize_operation(field, main_operator, secondary_operator, value)
54
+ end
55
+
56
+ # Inform if the operation has a operator
57
+ # @return [Boolean] True if the operation needs an operator, false otherwise.
58
+ def self.operator?
59
+ self.const_defined?('HAS_OPERATOR') && self.const_get('HAS_OPERATOR')
60
+ end
61
+
62
+ # Initialize the operators (both main and secondary)
63
+ # When the operator is an Array, it means it actually is two different operators. The first one will be applied
64
+ # to the main operation, and the second one, to the lookup.
65
+ # e.g. selector 'created_at__time__gt' contains two operators, 'time' and 'gt'.
66
+ # @return [Array<String>] Array with both operations.
67
+ # First element will be the main operation, the second one will be
68
+ # the secondary operation. If there is no secondary operation, the second item will be nil.
69
+ def self.initialize_operators(operator)
70
+ secondary_operator = nil
71
+ if operator.class == Array
72
+ secondary_operator = operator[1]
73
+ operator = operator[0]
74
+ end
75
+ [operator, secondary_operator]
76
+ end
77
+
78
+ # Initialize the operation
79
+ # @param field [String] Field name that takes part in the selection operation.
80
+ # @param operator [Symbol] Operator name that defines which operation will be used.
81
+ # @param secondary_operator [Symbol] Some operations have a particular operation that's it.
82
+ # @param value [String, Integer] Value used in the selection.
83
+ # @return [Babik::Selection::Operation::Base] A SQL selection operation.
84
+ def self.initialize_operation(field, operator, secondary_operator, value)
85
+ operation_class_name = Babik::Selection::Operation::CORRESPONDENCE[operator.to_sym]
86
+ raise "Unknown lookup #{operator}" unless operation_class_name
87
+ operation_class = Object.const_get("Babik::Selection::Operation::#{operation_class_name}")
88
+ # If there is a secondary operator, pass it to the operation
89
+ if secondary_operator || operation_class.operator?
90
+ return operation_class.new(field, secondary_operator, value)
91
+ end
92
+ # Otherwise, return the operation
93
+ operation_class.new(field, value)
94
+ end
95
+
96
+ # Special conversion of operations
97
+ def self.special_cases(field, operator, value)
98
+ return field, 'in', value if operator == 'equal' && [Babik::QuerySet::Base, Array].include?(value.class)
99
+ self.date_special_cases(field, operator, value)
100
+ end
101
+
102
+ # Special conversion of operations for date lookup
103
+ def self.date_special_cases(field, operator, value)
104
+ return field, 'between', [value.beginning_of_day, value.end_of_day] if operator == 'date' && value.is_a?(::Date)
105
+ return field, 'between', [Time(value.year, 1, 1).beginning_of_day, Time(value.year, 12, 31).end_of_day] if operator == 'year' && value.is_a?(::Date)
106
+ [field, operator, value]
107
+ end
108
+
109
+ # Escape a string
110
+ def self.escape(str)
111
+ Babik::Database.escape(str)
112
+ end
113
+ end
114
+
115
+ # Binary operation
116
+ # That's it ?field <operator> ?value
117
+ # Most operations will have this format
118
+ class BinaryOperation < Base
119
+ def initialize(field, value)
120
+ super(field, "?field #{self.class::SQL_OPERATOR} ?value", value)
121
+ end
122
+ end
123
+
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'babik/queryset/lib/selection/operation/base'
4
+
5
+ module Babik
6
+ module Selection
7
+ # SQL operation module
8
+ module Operation
9
+
10
+ # Check the DBMS is one of the supported ones
11
+ module ValidDBMS
12
+ SUPPORTED_DB_ADAPTERS = %i[mariadb mysql2 postgresql sqlite3].freeze
13
+ def assert_dbms
14
+ dbms = db_engine.to_sym
15
+ raise "Invalid dbms #{db_engine}. Only mysql, postgresql, and sqlite3 are accepted" unless SUPPORTED_DB_ADAPTERS.include?(dbms)
16
+ end
17
+ end
18
+
19
+ # Each one of the operations over date fields (date, year, month, day, etc.)
20
+ class DateOperation < Base
21
+ include ValidDBMS
22
+
23
+ HAS_OPERATOR = true
24
+ def initialize(field, operator, value)
25
+ assert_dbms
26
+ operator ||= 'equal'
27
+ @operator = operator
28
+ # In the left-hand of the main operator lies the sql_function
29
+ # that will extract the desired part of the datetime
30
+ # This function represents the field as #field, not as ?field
31
+ # to avoid having replacement issues
32
+ code_for_sql_function = sql_function
33
+ main_operation = Base.factory(code_for_sql_function, operator, value)
34
+ # Replacement mechanism only understand ?field and not #field,
35
+ # so replace #field for ?field and let it work
36
+ main_operation_sql_code = main_operation.sql_operation.sub('#field', '?field')
37
+ super(field, main_operation_sql_code, value)
38
+ end
39
+
40
+ def sql_function
41
+ raise NotImplementedError
42
+ end
43
+ end
44
+
45
+ # Year date operation
46
+ class Year < DateOperation
47
+ def sql_function
48
+ dbms_adapter = db_engine
49
+ return 'YEAR(#field)' if dbms_adapter == 'mysql2'
50
+ return 'EXTRACT(YEAR FROM #field)' if dbms_adapter == 'postgresql'
51
+ return 'strftime(\'%Y\', #field)' if dbms_adapter == 'sqlite3'
52
+ raise NotImplementedError, "#{self.class} lookup not implemented for #{dbms_adapter}"
53
+ end
54
+ end
55
+
56
+ # Quarter where the date is operation
57
+ class Quarter < DateOperation
58
+ def sql_function
59
+ dbms_adapter = db_engine
60
+ return 'QUARTER(#field)' if dbms_adapter == 'mysql2'
61
+ return 'EXTRACT(QUARTER FROM #field)' if dbms_adapter == 'postgresql'
62
+ return '(CAST(strftime(\'%m\', #field) AS INTEGER) + 2) / 3' if dbms_adapter == 'sqlite3'
63
+ raise NotImplementedError, "#{self.class} lookup not implemented for #{dbms_adapter}"
64
+ end
65
+ end
66
+
67
+ # Month date operation
68
+ class Month < DateOperation
69
+ def initialize(field, operator, value)
70
+ value = format('%02d', value) if db_engine == 'sqlite3'
71
+ super(field, operator, value)
72
+ end
73
+
74
+ def sql_function
75
+ dbms_adapter = db_engine
76
+ return 'MONTH(#field)' if dbms_adapter == 'mysql2'
77
+ return 'EXTRACT(MONTH FROM #field)' if dbms_adapter == 'postgresql'
78
+ return 'strftime(\'%m\', #field)' if dbms_adapter == 'sqlite3'
79
+ raise NotImplementedError, "#{self.class} lookup not implemented for #{dbms_adapter}"
80
+ end
81
+ end
82
+
83
+ # Day of month date operation
84
+ class Day < DateOperation
85
+ def initialize(field, operator, value)
86
+ value = format('%02d', value) if db_engine == 'sqlite3'
87
+ super(field, operator, value)
88
+ end
89
+
90
+ def sql_function
91
+ dbms_adapter = db_engine
92
+ return 'DAYOFMONTH(#field)' if dbms_adapter == 'mysql2'
93
+ return 'EXTRACT(DAY FROM #field)' if dbms_adapter == 'postgresql'
94
+ return 'strftime(\'%d\', #field)' if dbms_adapter == 'sqlite3'
95
+ raise NotImplementedError, "#{self.class} lookup not implemented for #{dbms_adapter}"
96
+ end
97
+ end
98
+
99
+ # WeekDay (1-7, sunday to monday) date operation
100
+ class WeekDay < DateOperation
101
+
102
+ def initialize(field, operator, value)
103
+ value = format('%d', value) if db_engine == 'sqlite3'
104
+ super(field, operator, value)
105
+ end
106
+
107
+ def sql_function
108
+ dbms_adapter = db_engine
109
+ return 'DAYOFWEEK(#field) - 1' if dbms_adapter == 'mysql2'
110
+ return 'EXTRACT(DOW FROM #field)' if dbms_adapter == 'postgresql'
111
+ return 'strftime(\'%w\', #field)' if dbms_adapter == 'sqlite3'
112
+ raise NotImplementedError, "#{self.class} lookup not implemented for #{dbms_adapter}"
113
+ end
114
+ end
115
+
116
+ # ISO Week of year (1-52/53) from date operation
117
+ #
118
+ class Week < DateOperation
119
+
120
+ def sql_function
121
+ dbms_adapter = db_engine
122
+ return 'WEEK(#field, 3)' if dbms_adapter == 'mysql2'
123
+ return 'EXTRACT(WEEK FROM #field)' if dbms_adapter == 'postgresql'
124
+ return '(strftime(\'%j\', date(#field, \'-3 days\', \'weekday 4\')) - 1) / 7 + 1' if dbms_adapter == 'sqlite3'
125
+ raise NotImplementedError, "#{self.class} lookup not implemented for #{dbms_adapter}"
126
+ end
127
+ end
128
+
129
+ # Hour date operation
130
+ class Hour < DateOperation
131
+
132
+ def sql_function
133
+ dbms_adapter = db_engine
134
+ return 'HOUR(#field)' if dbms_adapter == 'mysql2'
135
+ return 'EXTRACT(HOUR FROM #field)' if dbms_adapter == 'postgresql'
136
+ return 'CAST(strftime(\'%H\', #field) AS INTEGER)' if dbms_adapter == 'sqlite3'
137
+ raise NotImplementedError, "#{self.class} lookup not implemented for #{dbms_adapter}"
138
+ end
139
+ end
140
+
141
+ # Minute date operation
142
+ class Minute < DateOperation
143
+
144
+ def sql_function
145
+ dbms_adapter = db_engine
146
+ return 'MINUTE(#field)' if dbms_adapter == 'mysql2'
147
+ return 'EXTRACT(MINUTE FROM #field)' if dbms_adapter == 'postgresql'
148
+ return 'CAST(strftime(\'%M\', #field) AS INTEGER)' if dbms_adapter == 'sqlite3'
149
+ raise NotImplementedError, "#{self.class} lookup not implemented for #{dbms_adapter}"
150
+ end
151
+ end
152
+
153
+ # Second date operation
154
+ class Second < DateOperation
155
+
156
+ def sql_function
157
+ dbms_adapter = db_engine
158
+ return 'SECOND(#field)' if dbms_adapter == 'mysql2'
159
+ return 'FLOOR(EXTRACT(SECOND FROM #field))' if dbms_adapter == 'postgresql'
160
+ return 'CAST(strftime(\'%S\', #field) AS INTEGER)' if dbms_adapter == 'sqlite3'
161
+ raise NotImplementedError, "#{self.class} lookup not implemented for #{dbms_adapter}"
162
+ end
163
+ end
164
+
165
+ # Time date operation
166
+ class Time < DateOperation
167
+ def sql_function
168
+ dbms_adapter = db_engine
169
+ return 'DATE_FORMAT(#field, \'%H:%i:%s\')' if dbms_adapter == 'mysql2'
170
+ return 'date_trunc(\'second\', #field::time)' if dbms_adapter == 'postgresql'
171
+ return 'strftime(\'%H:%M:%S\', #field)' if dbms_adapter == 'sqlite3'
172
+ raise NotImplementedError, "#{self.class} lookup not implemented for #{dbms_adapter}"
173
+ end
174
+ end
175
+
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,201 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'babik/queryset/lib/selection/operation/base'
4
+ require 'babik/queryset/lib/selection/operation/date'
5
+ require 'babik/queryset/lib/selection/operation/regex'
6
+
7
+ module Babik
8
+ module Selection
9
+ # SQL operation module
10
+ module Operation
11
+
12
+ # When two values are not equal
13
+ class Different < BinaryOperation
14
+ SQL_OPERATOR = '<>'
15
+ end
16
+
17
+ # Operations that in case a nil is passed will convert the equality comparison to IS NULL
18
+ class IfNotNullOperation < Base
19
+ SQL_OPERATOR = '='
20
+ def initialize(field, value)
21
+ if value.nil?
22
+ super(field, '?field IS NULL', value)
23
+ else
24
+ super(field, "?field #{SQL_OPERATOR} ?value", value)
25
+ end
26
+ end
27
+ end
28
+
29
+ # Equal operation
30
+ class Equal < IfNotNullOperation
31
+ SQL_OPERATOR = '='
32
+ end
33
+
34
+ # Exact operation
35
+ class Exact < IfNotNullOperation
36
+ SQL_OPERATOR = 'LIKE'
37
+ end
38
+
39
+ # Exact case-insensitive operation
40
+ class IExact < Exact
41
+ SQL_OPERATOR = 'ILIKE'
42
+ end
43
+
44
+ # IN operation
45
+ class In < Base
46
+ def initialize(field, value)
47
+ _init_value(value)
48
+ super(field, '?field IN ?value', @value)
49
+ end
50
+
51
+ def _init_value(value)
52
+ if value.class == Array
53
+ values = value.map do |value_i|
54
+ if value_i.is_a?(String)
55
+ self.class.escape(value_i)
56
+ elsif value_i.is_a?(ActiveRecord::Base)
57
+ value_i.id
58
+ else
59
+ value_i
60
+ end
61
+ end
62
+ @value = "(#{values.join(', ')})"
63
+ elsif value.class == Babik::QuerySet::Base
64
+ @value = "(#{value.sql.select})"
65
+ elsif value.class == String
66
+ @value = "('#{self.class.escape(value)}')"
67
+ else
68
+ @value = "(#{value})"
69
+ end
70
+ end
71
+
72
+ def _init_sql_operation
73
+ @sql_operation = @sql_operation_template.sub('?field', @field).sub('?value', @value)
74
+ end
75
+ end
76
+
77
+ # IS NULL operation
78
+ class IsNull < Base
79
+ def initialize(field, value)
80
+ sql_operation = value ? '?field IS NULL' : '?field IS NOT NULL'
81
+ super(field, sql_operation, value)
82
+ end
83
+ end
84
+
85
+ # Less than comparison
86
+ class LessThan < BinaryOperation
87
+ SQL_OPERATOR = '<'
88
+ end
89
+
90
+ # Less than or equal comparison
91
+ class LessThanOrEqual < BinaryOperation
92
+ SQL_OPERATOR = '<='
93
+ end
94
+
95
+ # Greater than comparison
96
+ class GreaterThan < BinaryOperation
97
+ SQL_OPERATOR = '>'
98
+ end
99
+
100
+ # Greater than or equal comparison
101
+ class GreaterThanOrEqual < BinaryOperation
102
+ SQL_OPERATOR = '>='
103
+ end
104
+
105
+ # Between comparison (check the value is between two different values)
106
+ class Between < Base
107
+ def initialize(field, value)
108
+ super(field, '?field BETWEEN ?value1 AND ?value2', value)
109
+ end
110
+
111
+ def _init_sql_operation
112
+ if @value.class == Array
113
+ if [@value[0], @value[1]].map { |v| [DateTime, Date, Time].include?(v.class) } == [true, true]
114
+ value1 = "'#{@value[0].utc.to_s(:db)}'"
115
+ value2 = "'#{@value[1].utc.to_s(:db)}'"
116
+ else
117
+ value1 = self.class.escape(@value[0])
118
+ value2 = self.class.escape(@value[1])
119
+ end
120
+ @sql_operation = @sql_operation_template.sub('?field', @field).sub('?value1', value1).sub('?value2', value2)
121
+ else
122
+ raise 'Array is needed if operator is between'
123
+ end
124
+ end
125
+ end
126
+
127
+ class StartsWith < BinaryOperation
128
+ SQL_OPERATOR = 'LIKE'
129
+ def _init_sql_operation
130
+ escaped_value = self.class.escape("#{@value}%")
131
+ @sql_operation = @sql_operation_template.sub('?field', @field).sub('?value', escaped_value)
132
+ end
133
+ end
134
+
135
+ class IStartsWith < StartsWith
136
+ SQL_OPERATOR = 'ILIKE'
137
+ end
138
+
139
+ class EndsWith < BinaryOperation
140
+ SQL_OPERATOR = 'LIKE'
141
+ def _init_sql_operation
142
+ escaped_value = self.class.escape("%#{@value}")
143
+ @sql_operation = @sql_operation.sub('?field', @field).sub('?value', escaped_value)
144
+ end
145
+ end
146
+
147
+ class IEndsWith < StartsWith
148
+ SQL_OPERATOR = 'ILIKE'
149
+ end
150
+
151
+ class Contains < BinaryOperation
152
+ SQL_OPERATOR = 'LIKE'
153
+ def _init_sql_operation
154
+ escaped_value = self.class.escape("%#{@value}%")
155
+ @sql_operation = @sql_operation_template.sub('?field', @field).sub('?value', escaped_value)
156
+ end
157
+ end
158
+
159
+ class IContains < Contains
160
+ SQL_OPERATOR = 'ILIKE'
161
+ end
162
+
163
+ CORRESPONDENCE = {
164
+ default: Equal,
165
+ equal: Equal,
166
+ equals: Equal,
167
+ equals_to: Equal,
168
+ exact: Exact,
169
+ iexact: IExact,
170
+ different: Different,
171
+ in: In,
172
+ isnull: IsNull,
173
+ lt: LessThan,
174
+ lte: LessThanOrEqual,
175
+ gt: GreaterThan,
176
+ gte: GreaterThanOrEqual,
177
+ between: Between,
178
+ range: Between,
179
+ startswith: StartsWith,
180
+ endswith: EndsWith,
181
+ contains: Contains,
182
+ istartswith: IStartsWith,
183
+ iendswith: IEndsWith,
184
+ icontains: IContains,
185
+ regex: Babik::Selection::Operation::Regex,
186
+ iregex: IRegex,
187
+ year: Year,
188
+ quarter: Quarter,
189
+ month: Month,
190
+ day: Day,
191
+ week_day: WeekDay,
192
+ week: Week,
193
+ hour: Hour,
194
+ minute: Minute,
195
+ second: Second,
196
+ time: Babik::Selection::Operation::Time
197
+ }.freeze
198
+
199
+ end
200
+ end
201
+ end