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.
- checksums.yaml +7 -0
- data/Gemfile +16 -0
- data/README.md +718 -0
- data/Rakefile +18 -0
- data/lib/babik.rb +122 -0
- data/lib/babik/database.rb +16 -0
- data/lib/babik/queryset.rb +154 -0
- data/lib/babik/queryset/components/aggregation.rb +172 -0
- data/lib/babik/queryset/components/limit.rb +22 -0
- data/lib/babik/queryset/components/order.rb +161 -0
- data/lib/babik/queryset/components/projection.rb +118 -0
- data/lib/babik/queryset/components/select_related.rb +78 -0
- data/lib/babik/queryset/components/sql_renderer.rb +99 -0
- data/lib/babik/queryset/components/where.rb +43 -0
- data/lib/babik/queryset/lib/association/foreign_association_chain.rb +97 -0
- data/lib/babik/queryset/lib/association/select_related_association_chain.rb +32 -0
- data/lib/babik/queryset/lib/condition.rb +103 -0
- data/lib/babik/queryset/lib/field.rb +34 -0
- data/lib/babik/queryset/lib/join/association_joiner.rb +39 -0
- data/lib/babik/queryset/lib/join/join.rb +86 -0
- data/lib/babik/queryset/lib/selection/config.rb +19 -0
- data/lib/babik/queryset/lib/selection/foreign_selection.rb +39 -0
- data/lib/babik/queryset/lib/selection/local_selection.rb +40 -0
- data/lib/babik/queryset/lib/selection/operation/base.rb +126 -0
- data/lib/babik/queryset/lib/selection/operation/date.rb +178 -0
- data/lib/babik/queryset/lib/selection/operation/operations.rb +201 -0
- data/lib/babik/queryset/lib/selection/operation/regex.rb +58 -0
- data/lib/babik/queryset/lib/selection/path/foreign_path.rb +50 -0
- data/lib/babik/queryset/lib/selection/path/local_path.rb +44 -0
- data/lib/babik/queryset/lib/selection/path/path.rb +23 -0
- data/lib/babik/queryset/lib/selection/select_related_selection.rb +38 -0
- data/lib/babik/queryset/lib/selection/selection.rb +19 -0
- data/lib/babik/queryset/lib/update/assignment.rb +108 -0
- data/lib/babik/queryset/mixins/aggregatable.rb +17 -0
- data/lib/babik/queryset/mixins/bounded.rb +38 -0
- data/lib/babik/queryset/mixins/clonable.rb +52 -0
- data/lib/babik/queryset/mixins/countable.rb +44 -0
- data/lib/babik/queryset/mixins/deletable.rb +13 -0
- data/lib/babik/queryset/mixins/distinguishable.rb +27 -0
- data/lib/babik/queryset/mixins/filterable.rb +51 -0
- data/lib/babik/queryset/mixins/limitable.rb +88 -0
- data/lib/babik/queryset/mixins/lockable.rb +31 -0
- data/lib/babik/queryset/mixins/none.rb +16 -0
- data/lib/babik/queryset/mixins/projectable.rb +34 -0
- data/lib/babik/queryset/mixins/related_selector.rb +28 -0
- data/lib/babik/queryset/mixins/set_operations.rb +32 -0
- data/lib/babik/queryset/mixins/sortable.rb +49 -0
- data/lib/babik/queryset/mixins/sql_renderizable.rb +17 -0
- data/lib/babik/queryset/mixins/updatable.rb +14 -0
- data/lib/babik/queryset/templates/default/delete/main.sql.erb +14 -0
- data/lib/babik/queryset/templates/default/select/components/aggregation.sql.erb +5 -0
- data/lib/babik/queryset/templates/default/select/components/from.sql.erb +16 -0
- data/lib/babik/queryset/templates/default/select/components/from_set.sql.erb +3 -0
- data/lib/babik/queryset/templates/default/select/components/from_table.sql.erb +2 -0
- data/lib/babik/queryset/templates/default/select/components/limit.sql.erb +10 -0
- data/lib/babik/queryset/templates/default/select/components/order_by.sql.erb +9 -0
- data/lib/babik/queryset/templates/default/select/components/projection.sql.erb +7 -0
- data/lib/babik/queryset/templates/default/select/components/select_related.sql.erb +26 -0
- data/lib/babik/queryset/templates/default/select/components/where.sql.erb +39 -0
- data/lib/babik/queryset/templates/default/select/main.sql.erb +42 -0
- data/lib/babik/queryset/templates/default/update/main.sql.erb +15 -0
- data/lib/babik/queryset/templates/mssql/select/components/limit.sql.erb +8 -0
- data/lib/babik/queryset/templates/mssql/select/components/order_by.sql.erb +21 -0
- data/lib/babik/queryset/templates/mysql2/delete/main.sql.erb +15 -0
- data/lib/babik/queryset/templates/mysql2/update/main.sql.erb +18 -0
- data/lib/babik/queryset/templates/sqlite3/select/components/from_set.sql.erb +5 -0
- data/test/config/db/schema.rb +83 -0
- data/test/config/models/bad_post.rb +5 -0
- data/test/config/models/bad_tag.rb +5 -0
- data/test/config/models/category.rb +4 -0
- data/test/config/models/geozone.rb +6 -0
- data/test/config/models/group.rb +5 -0
- data/test/config/models/group_user.rb +5 -0
- data/test/config/models/post.rb +24 -0
- data/test/config/models/post_tag.rb +5 -0
- data/test/config/models/tag.rb +5 -0
- data/test/config/models/user.rb +6 -0
- data/test/delete/delete_test.rb +60 -0
- data/test/delete/foreign_conditions_delete_test.rb +57 -0
- data/test/delete/local_conditions_delete_test.rb +20 -0
- data/test/enable_coverage.rb +17 -0
- data/test/lib/selection/operation/log/test-queries.log +1 -0
- data/test/lib/selection/operation/test_date.rb +131 -0
- data/test/lib/selection/operation/test_regex.rb +55 -0
- data/test/other/clone_test.rb +129 -0
- data/test/other/escape_test.rb +21 -0
- data/test/other/inverse_of_required_test.rb +33 -0
- data/test/select/aggregate_test.rb +151 -0
- data/test/select/bounds_test.rb +46 -0
- data/test/select/count_test.rb +147 -0
- data/test/select/distinct_test.rb +38 -0
- data/test/select/exclude_test.rb +72 -0
- data/test/select/filter_from_object_test.rb +125 -0
- data/test/select/filter_test.rb +207 -0
- data/test/select/for_update_test.rb +19 -0
- data/test/select/foreign_selection_test.rb +60 -0
- data/test/select/get_test.rb +40 -0
- data/test/select/limit_test.rb +109 -0
- data/test/select/local_selection_test.rb +24 -0
- data/test/select/lookup_test.rb +208 -0
- data/test/select/none_test.rb +40 -0
- data/test/select/order_test.rb +165 -0
- data/test/select/project_test.rb +107 -0
- data/test/select/select_related_test.rb +124 -0
- data/test/select/subquery_test.rb +50 -0
- data/test/set_operations/basic_usage_test.rb +121 -0
- data/test/test_helper.rb +55 -0
- data/test/update/update_test.rb +93 -0
- 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
|