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,58 @@
|
|
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
|
+
# Match by case sensitive regex
|
11
|
+
class Regex < Base
|
12
|
+
def initialize(field, value)
|
13
|
+
value = value.inspect[1..-2] if value.class == Regexp
|
14
|
+
value = value[1..-2] if value.class == String && value[0] == '/' && value[-1] == '/'
|
15
|
+
value = _mysql2_convert_regex(value) if db_engine == 'mysql2'
|
16
|
+
super(field, "?field #{operator} ?value", value)
|
17
|
+
end
|
18
|
+
|
19
|
+
def operator
|
20
|
+
dbms_adapter = db_engine
|
21
|
+
return 'REGEXP' if dbms_adapter == 'mysql2'
|
22
|
+
return '~' if dbms_adapter == 'postgresql'
|
23
|
+
return 'REGEXP' if dbms_adapter == 'sqlite3'
|
24
|
+
raise NotImplementedError, "Invalid dbms #{dbms_adapter}. Only mysql, postgresql, and sqlite3 are accepted"
|
25
|
+
end
|
26
|
+
|
27
|
+
def _mysql2_convert_regex(value)
|
28
|
+
replacements = { '\\d' => '[0-9]', '\\w' => '[a-zA-Z]' }
|
29
|
+
replacements.each do |pcre_pattern, mysql_pattern|
|
30
|
+
value = value.gsub(pcre_pattern, mysql_pattern)
|
31
|
+
end
|
32
|
+
value
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Match by case insensitive regex
|
37
|
+
class IRegex < Regex
|
38
|
+
|
39
|
+
def initialize(field, value)
|
40
|
+
value = value.inspect[1..-2] if value.class == Regexp
|
41
|
+
value = value[1..-2] if value.class == String && value[0] == '/' && value[-1] == '/'
|
42
|
+
value = "(?i)#{value}" if db_engine == 'sqlite3'
|
43
|
+
field = "LOWER(#{field})" if db_engine == 'mysql2'
|
44
|
+
super(field, value)
|
45
|
+
end
|
46
|
+
|
47
|
+
def operator
|
48
|
+
dbms_adapter = db_engine
|
49
|
+
return 'REGEXP' if dbms_adapter == 'mysql2'
|
50
|
+
return '~*' if dbms_adapter == 'postgresql'
|
51
|
+
return 'REGEXP' if dbms_adapter == 'sqlite3'
|
52
|
+
raise NotImplementedError, "Invalid dbms #{dbms_adapter}. Only mysql, postgresql, and sqlite3 are accepted"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'babik/queryset/lib/selection/config'
|
4
|
+
require 'babik/queryset/lib/join/association_joiner'
|
5
|
+
require 'babik/queryset/lib/association/foreign_association_chain'
|
6
|
+
|
7
|
+
module Babik
|
8
|
+
module Selection
|
9
|
+
module Path
|
10
|
+
# Foreign path
|
11
|
+
# A foreign path is a succession of associations ending optionally in an operator
|
12
|
+
# if operator is not present, equal is supposed.
|
13
|
+
class ForeignPath
|
14
|
+
RELATIONSHIP_SEPARATOR = Babik::Selection::Config::RELATIONSHIP_SEPARATOR
|
15
|
+
OPERATOR_SEPARATOR = Babik::Selection::Config::OPERATOR_SEPARATOR
|
16
|
+
|
17
|
+
attr_reader :model, :selection_path, :selected_field
|
18
|
+
|
19
|
+
delegate :left_joins_by_alias, to: :@association_joiner
|
20
|
+
delegate :target_alias, to: :@association_joiner
|
21
|
+
delegate :associations, to: :@association_chain
|
22
|
+
|
23
|
+
# Construct a foreign path
|
24
|
+
# A foreign path will be used with a value as a foreign selection to filter
|
25
|
+
# a model with foreign conditions
|
26
|
+
# @param model [ActiveRecord::Base] model that is the object of the foreign path.
|
27
|
+
# @param selection_path [String, Symbol] Association path with an operator. e.g.:
|
28
|
+
# posts::category__in
|
29
|
+
# author::posts::tags
|
30
|
+
# creation_at__date__gte
|
31
|
+
#
|
32
|
+
def initialize(model, selection_path)
|
33
|
+
@model = model
|
34
|
+
@selection_path = selection_path.dup
|
35
|
+
@association_path = selection_path.to_s.split(RELATIONSHIP_SEPARATOR)
|
36
|
+
selection_path = @association_path.pop
|
37
|
+
@selected_field, @operator = selection_path.split(OPERATOR_SEPARATOR)
|
38
|
+
@operator ||= 'equal'
|
39
|
+
_initialize_associations
|
40
|
+
end
|
41
|
+
|
42
|
+
# Initialize associations
|
43
|
+
def _initialize_associations
|
44
|
+
@association_chain = Babik::Association::ForeignAssociationChain.new(@model, @association_path, @selection_path)
|
45
|
+
@association_joiner = Babik::QuerySet::Join::AssociationJoiner.new(@association_chain.associations)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'babik/queryset/lib/selection/config'
|
4
|
+
|
5
|
+
module Babik
|
6
|
+
module Selection
|
7
|
+
module Path
|
8
|
+
|
9
|
+
# Local path
|
10
|
+
class LocalPath
|
11
|
+
OPERATOR_SEPARATOR = Babik::Selection::Config::OPERATOR_SEPARATOR
|
12
|
+
|
13
|
+
attr_reader :model, :selection_path, :selected_field, :operator, :secondary_operator
|
14
|
+
|
15
|
+
# Construct a local field path
|
16
|
+
# @param model [ActiveRecord::Base] model whose field will be used.
|
17
|
+
# @param selection_path [String] selection path. Of the form <field>__<operator>. e.g. first_name__equal, stars__gt
|
18
|
+
# If no operator is given (first_name), 'equal' will be used.
|
19
|
+
def initialize(model, selection_path)
|
20
|
+
@model = model
|
21
|
+
@selection_path = selection_path.dup
|
22
|
+
@selected_field, @operator, @secondary_operator = @selection_path.to_s.split(OPERATOR_SEPARATOR)
|
23
|
+
# By default, if no operator is given, 'equal' will be used
|
24
|
+
@operator ||= 'equal'
|
25
|
+
end
|
26
|
+
|
27
|
+
# Return the target table alias.
|
28
|
+
# That is alias of the model table.
|
29
|
+
# For the moment, actually, return the name of this model's table.
|
30
|
+
# @return [String] alias of the model table.
|
31
|
+
def target_alias
|
32
|
+
@model.table_name
|
33
|
+
end
|
34
|
+
|
35
|
+
# A local selection has no related left joins
|
36
|
+
# @return [Hash] Empty hash.
|
37
|
+
def left_joins_by_alias
|
38
|
+
{}
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'babik/queryset/lib/selection/config'
|
4
|
+
require 'babik/queryset/lib/selection/path/foreign_path'
|
5
|
+
require 'babik/queryset/lib/selection/path/local_path'
|
6
|
+
|
7
|
+
module Babik
|
8
|
+
module Selection
|
9
|
+
module Path
|
10
|
+
# Represents a factory class for ForeignPath & LocalPath
|
11
|
+
class Factory
|
12
|
+
|
13
|
+
# Factory Method used to create local and foreign selections
|
14
|
+
def self.build(model, selection_path)
|
15
|
+
is_foreign = selection_path.match?(Babik::Selection::Config::RELATIONSHIP_SEPARATOR)
|
16
|
+
return Babik::Selection::Path::ForeignPath.new(model, selection_path) if is_foreign
|
17
|
+
Babik::Selection::Path::LocalPath.new(model, selection_path)
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'babik/queryset/lib/join/association_joiner'
|
4
|
+
require 'babik/queryset/lib/join/join'
|
5
|
+
require 'babik/queryset/lib/selection/selection'
|
6
|
+
require 'babik/queryset/lib/selection/config'
|
7
|
+
require 'babik/queryset/lib/association/select_related_association_chain'
|
8
|
+
require 'babik/queryset/lib/selection/path/foreign_path'
|
9
|
+
|
10
|
+
module Babik
|
11
|
+
module Selection
|
12
|
+
|
13
|
+
# Abstraction of a selection used in select_related operation
|
14
|
+
class SelectRelatedSelection
|
15
|
+
RELATIONSHIP_SEPARATOR = Babik::Selection::Config::RELATIONSHIP_SEPARATOR
|
16
|
+
attr_reader :model, :selection_path, :association_path, :target_model, :id
|
17
|
+
|
18
|
+
delegate :left_joins_by_alias, to: :@association_joiner
|
19
|
+
delegate :target_alias, to: :@association_joiner
|
20
|
+
|
21
|
+
def initialize(model, selection_path)
|
22
|
+
@model = model
|
23
|
+
@selection_path = selection_path.dup
|
24
|
+
@association_path = selection_path.to_s.split(RELATIONSHIP_SEPARATOR)
|
25
|
+
@id = @association_path.join('__')
|
26
|
+
|
27
|
+
_initialize_associations
|
28
|
+
@target_model = @association_chain.target_model
|
29
|
+
end
|
30
|
+
|
31
|
+
def _initialize_associations
|
32
|
+
@association_chain = Babik::Association::SelectRelatedAssociationChain.new(@model, @association_path, @selection_path)
|
33
|
+
@association_joiner = Babik::QuerySet::Join::AssociationJoiner.new(@association_chain.associations)
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'babik/queryset/lib/selection/config'
|
4
|
+
|
5
|
+
module Babik
|
6
|
+
module Selection
|
7
|
+
# Represents a filter selection (that can be filtered in WHERE)
|
8
|
+
class Base
|
9
|
+
|
10
|
+
# Factory Method used to create local and foreign selections
|
11
|
+
def self.factory(model, selection_path, value)
|
12
|
+
is_foreign_selection = selection_path.match?(Babik::Selection::Config::RELATIONSHIP_SEPARATOR)
|
13
|
+
return Babik::Selection::ForeignSelection.new(model, selection_path, value) if is_foreign_selection
|
14
|
+
Babik::Selection::LocalSelection.new(model, selection_path, value)
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Common module for Babik library
|
4
|
+
module Babik
|
5
|
+
module QuerySet
|
6
|
+
# Update operation module
|
7
|
+
module Update
|
8
|
+
# Field assignment module
|
9
|
+
module Assignment
|
10
|
+
# Return the field prepared for the UPDATE operation.
|
11
|
+
# Used when rendering the SQL template
|
12
|
+
# @param model [ActiveRecord::Base] model this field belongs to.
|
13
|
+
# @param field [String] field to be updated.
|
14
|
+
# @return [String] Field prepared to be inserted in the left part of a SQL UPDATE assignment.
|
15
|
+
def self.sql_field(model, field)
|
16
|
+
field = Babik::Table::Field.new(model, field)
|
17
|
+
field.real_field
|
18
|
+
end
|
19
|
+
|
20
|
+
# Return the value prepared for the UPDATE operation.
|
21
|
+
# Used when rendering the SQL template
|
22
|
+
# @param update_field_value [Operation, Function, String, ActiveRecord::BASE] field to be updated.
|
23
|
+
# if Operation, an arithmetic operation based on other field of the record will be applied (+, -, * ...)
|
24
|
+
# if Function, a function will be called.
|
25
|
+
# The parameters of the function can ben any other field of the record.
|
26
|
+
# if String, a escaped version of the value will be returned.
|
27
|
+
# if ActiveRecord::Base, the id of the object will be returned.
|
28
|
+
# Otherwise, the value as-is will be returned.
|
29
|
+
# @return [String] Field prepared to be inserted in the left part of a SQL UPDATE assignment.
|
30
|
+
def self.sql_value(update_field_value)
|
31
|
+
return update_field_value.sql_value if update_field_value.is_a?(Operation) || update_field_value.is_a?(Function)
|
32
|
+
return _escape(update_field_value) if update_field_value.is_a?(String)
|
33
|
+
return update_field_value.id if update_field_value.is_a?(ActiveRecord::Base)
|
34
|
+
update_field_value
|
35
|
+
end
|
36
|
+
|
37
|
+
# Escape a value for database
|
38
|
+
# @param str [String] original string value.
|
39
|
+
# @return [String] escaped string value.
|
40
|
+
def self._escape(str)
|
41
|
+
Babik::Database.escape(str)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Represents a function operator that can be used in an UPDATE
|
45
|
+
# For example:
|
46
|
+
# UPDATE SET stars = ABS(stars)
|
47
|
+
class Function
|
48
|
+
def initialize(field, function_call)
|
49
|
+
@field = field
|
50
|
+
@function_call = function_call
|
51
|
+
end
|
52
|
+
|
53
|
+
# Return the right part of the assignment of the UPDATE statement.
|
54
|
+
# @return [String] right part of the assignment with the format defined by the function_call attribute.
|
55
|
+
def sql_value
|
56
|
+
@function_call
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Represents a table field. It will be used when an update field is based on its value an nothing else.
|
61
|
+
# For example:
|
62
|
+
# UPDATE SET stars = stars + 1
|
63
|
+
class Operation
|
64
|
+
def initialize(field, operation, value)
|
65
|
+
@field = field
|
66
|
+
@operation = operation
|
67
|
+
@value = value
|
68
|
+
end
|
69
|
+
|
70
|
+
# Return the right part of the assignment of the UPDATE statement.
|
71
|
+
# @return [String] right part of the assignment with the format <field> <operation> <value>.
|
72
|
+
def sql_value
|
73
|
+
"#{@field} #{@operation} #{@value}"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Decrement operation
|
78
|
+
class Decrement < Operation
|
79
|
+
def initialize(field, value = 1)
|
80
|
+
super(field, '-', value)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Increment operation
|
85
|
+
class Increment < Operation
|
86
|
+
def initialize(field, value = 1)
|
87
|
+
super(field, '+', value)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Multiplication operation
|
92
|
+
class Multiply < Operation
|
93
|
+
def initialize(field, value)
|
94
|
+
super(field, '*', value)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Division operation
|
99
|
+
class Divide < Operation
|
100
|
+
def initialize(field, value)
|
101
|
+
super(field, '/', value)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Babik
|
4
|
+
module QuerySet
|
5
|
+
# Functionality related to the aggregation selection
|
6
|
+
module Aggregatable
|
7
|
+
# Aggregate a set of objects.
|
8
|
+
# @param agg_functions [Hash{symbol: Babik.agg}] hash with the different aggregations that will be computed.
|
9
|
+
# @return [Hash{symbol: float}] Result of computing each one of the aggregations.
|
10
|
+
def aggregate(agg_functions)
|
11
|
+
@_aggregation = Babik::QuerySet::Aggregation.new(@model, agg_functions)
|
12
|
+
select_sql = sql.select
|
13
|
+
self.class._execute_sql(select_sql).first.transform_values(&:to_f).symbolize_keys
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Babik
|
4
|
+
module QuerySet
|
5
|
+
# Every QuerySet is bounded by its first and last items
|
6
|
+
module Bounded
|
7
|
+
|
8
|
+
# Return the first element given some order
|
9
|
+
# @param order [Array, String, Hash] ordering that will be applied to the QuerySet.
|
10
|
+
# See {Babik::QuerySet::Sortable#order_by}.
|
11
|
+
# @return [ActiveRecord::Base] First element according to the order.
|
12
|
+
def earliest(*order)
|
13
|
+
self.order_by(*order).first
|
14
|
+
end
|
15
|
+
|
16
|
+
# Return the first element of the QuerySet.
|
17
|
+
# @return [ActiveRecord::Base] First element of the QuerySet.
|
18
|
+
def first
|
19
|
+
self.all.first
|
20
|
+
end
|
21
|
+
|
22
|
+
# Return the last element of the QuerySet.
|
23
|
+
# @return [ActiveRecord::Base] Last element of the QuerySet.
|
24
|
+
def last
|
25
|
+
self.invert_order.all.first
|
26
|
+
end
|
27
|
+
|
28
|
+
# Return the last element given some order
|
29
|
+
# @param order [Array, String, Hash] ordering that will be applied to the QuerySet.
|
30
|
+
# See {Babik::QuerySet::Sortable#order_by}.
|
31
|
+
# @return [ActiveRecord::Base] Last element according to the order.
|
32
|
+
def latest(*order)
|
33
|
+
self.order_by(*order).last
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'deep_clone'
|
4
|
+
|
5
|
+
module Babik
|
6
|
+
module QuerySet
|
7
|
+
# Clone operation for the QuerySet
|
8
|
+
module Clonable
|
9
|
+
|
10
|
+
# Clone the queryset using ruby_deep_clone {https://github.com/gmodarelli/ruby-deepclone}.
|
11
|
+
# @return [QuerySet] Deep copy of this QuerySet.
|
12
|
+
def clone
|
13
|
+
DeepClone.clone(self)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Clone this QuerySet and apply the 'mutator_method' to it.
|
17
|
+
# @param mutator_method [Symbol] Name of the method.
|
18
|
+
# @param parameters [Array] Parameters passed to the method
|
19
|
+
# @return [QuerySet] The resultant QuerySet of applying the mutator to the clone of the caller object.
|
20
|
+
def mutate_clone(mutator_method, parameters = [])
|
21
|
+
clone_ = clone
|
22
|
+
if parameters.empty?
|
23
|
+
clone_.send(mutator_method)
|
24
|
+
else
|
25
|
+
clone_.send(mutator_method, *parameters)
|
26
|
+
end
|
27
|
+
clone_
|
28
|
+
end
|
29
|
+
|
30
|
+
# Check if the called method has a modifying version (a bang method). If that is the case
|
31
|
+
# it will be called on a clone of this instance. Otherwise, super will be called.
|
32
|
+
# @param name [String] method name
|
33
|
+
# @param args [String] method arguments
|
34
|
+
# @param _block [Proc] Proc that could be passed to the method. Not used.
|
35
|
+
# @return [QuerySet] Clone of this QuerySet (with method 'name' called on ), an empty QuerySet.
|
36
|
+
def method_missing(name, *args, &_block)
|
37
|
+
modifying_method = "#{name}!"
|
38
|
+
return mutate_clone(modifying_method.to_sym, args) if self.respond_to?(modifying_method)
|
39
|
+
super
|
40
|
+
end
|
41
|
+
|
42
|
+
# Check if the called method has a modifying version (a bang method).
|
43
|
+
# @return [Boolean] True if there is a modifying method with the requested method name
|
44
|
+
# in that case, return true, otherwise, return false.
|
45
|
+
def respond_to_missing?(name, *_args, &_block)
|
46
|
+
modifying_method = "#{name}!"
|
47
|
+
self.respond_to?(modifying_method)
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|