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
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# inside tasks/test.rake
|
2
|
+
require 'rake/testtask'
|
3
|
+
|
4
|
+
desc 'Run tests'
|
5
|
+
Rake::TestTask.new(:test) do |t|
|
6
|
+
t.libs.push 'test'
|
7
|
+
t.test_files = FileList['test/enable_coverage.rb', 'test/**/*_test.rb']
|
8
|
+
t.warning = ENV['warning']
|
9
|
+
t.verbose = ENV['verbose']
|
10
|
+
end
|
11
|
+
|
12
|
+
desc 'Generates a coverage report'
|
13
|
+
task :coverage do
|
14
|
+
ENV['COVERAGE'] = 'true'
|
15
|
+
Rake::Task['test'].execute
|
16
|
+
end
|
17
|
+
|
18
|
+
task default: :test
|
data/lib/babik.rb
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_record'
|
4
|
+
require_relative 'babik/queryset'
|
5
|
+
|
6
|
+
# Babik module
|
7
|
+
module Babik
|
8
|
+
|
9
|
+
# @!method included(base)
|
10
|
+
# Inject both class methods and instance methods to classes that include this mixin
|
11
|
+
# @param [Class] base Class to be extended by mixin.
|
12
|
+
def self.included(base)
|
13
|
+
base.send :include, InstanceMethods
|
14
|
+
base.extend ClassMethods
|
15
|
+
end
|
16
|
+
|
17
|
+
# All instance methods that are injected to ActiveRecord models
|
18
|
+
module InstanceMethods
|
19
|
+
|
20
|
+
# @!method objects(selection_path = nil)
|
21
|
+
# Get a queryset that contains the foreign model filtered by the current instance
|
22
|
+
# @param [String] selection_path Association name whose objects we want to return.
|
23
|
+
# @return [QuerySet] QuerySet with the foreign objects filtered by this instance.
|
24
|
+
def objects(selection_path = nil)
|
25
|
+
# Instance based deep association
|
26
|
+
instance_based_queryset = _objects_with_selection_path(selection_path)
|
27
|
+
return instance_based_queryset if instance_based_queryset
|
28
|
+
|
29
|
+
# Basic association to one (belongs_to and has_one)
|
30
|
+
to_one_result = self._objects_to_one(selection_path)
|
31
|
+
return to_one_result if to_one_result
|
32
|
+
|
33
|
+
# has_many direct relationship (default case)
|
34
|
+
self._objects_direct_has_many(selection_path)
|
35
|
+
end
|
36
|
+
|
37
|
+
# @!method _objects_with_selection_path(selection_path = nil)
|
38
|
+
# Return a QuerySet following the passed selection path.
|
39
|
+
# @param [String, Symbol, nil] selection_path Path of relationships that will be used as filter.
|
40
|
+
# If nil, a QuerySet with the current object selected will be returned. Otherwise, a QuerySet with the selection
|
41
|
+
# described by the __ and :: operators.
|
42
|
+
# @return [QuerySet] QuerySet for the selection_path passed as parameter.
|
43
|
+
def _objects_with_selection_path(selection_path = nil)
|
44
|
+
# By default, a nil selection_path means the caller object wants to return a QuerySet with only itself
|
45
|
+
return self.class.objects.filter(id: self.id) unless selection_path
|
46
|
+
|
47
|
+
selection_path = selection_path.to_s
|
48
|
+
is_a_selection_path = selection_path.include?(Babik::Selection::Config::RELATIONSHIP_SEPARATOR)
|
49
|
+
return nil unless is_a_selection_path
|
50
|
+
|
51
|
+
# If the selection path has more than one level deep, we have to build an instance-based query
|
52
|
+
selection_path_parts = selection_path.split(Babik::Selection::Config::RELATIONSHIP_SEPARATOR)
|
53
|
+
model_i = self.class
|
54
|
+
|
55
|
+
# The aim is to reverse the selection_path both
|
56
|
+
# - Relationships will come from target to source.
|
57
|
+
# - Direction: the instance will become the filter.
|
58
|
+
|
59
|
+
instance_selection_path_parts = []
|
60
|
+
|
61
|
+
# For each selection path part, invert the association and construct a
|
62
|
+
# new selection path for our instance-based query.
|
63
|
+
selection_path_parts.each do |association_name_i|
|
64
|
+
association_i = model_i.reflect_on_association(association_name_i.to_sym)
|
65
|
+
inverse_association_name_i = association_i.options.fetch(:inverse_of)
|
66
|
+
instance_selection_path_parts = [inverse_association_name_i] + instance_selection_path_parts
|
67
|
+
model_i = association_i.klass
|
68
|
+
end
|
69
|
+
|
70
|
+
# Construct a new selection path for our instance-based query
|
71
|
+
instance_selection_path = instance_selection_path_parts.join(Babik::Selection::Config::RELATIONSHIP_SEPARATOR)
|
72
|
+
model_i.objects.filter("#{instance_selection_path}::id": self.id)
|
73
|
+
end
|
74
|
+
|
75
|
+
# @!method _objects_to_one(association_name)
|
76
|
+
# Return a QuerySet with the relationship to one
|
77
|
+
# @param [String, Symbol] association_name Association name that identifies a relationship with other object.
|
78
|
+
# @return [QuerySet, nil] QuerySet based on the association_name, nil if the relationship is not found.
|
79
|
+
def _objects_to_one(association_name)
|
80
|
+
association_name_to_sym = association_name.to_sym
|
81
|
+
association = self.class.reflect_on_association(association_name_to_sym)
|
82
|
+
return nil unless association
|
83
|
+
# If the relationship is belongs_to or has_one, return a lone ActiveRecord model
|
84
|
+
return self.send(association_name_to_sym) if association.belongs_to? || association.has_one?
|
85
|
+
nil
|
86
|
+
end
|
87
|
+
|
88
|
+
# @!method _objects_direct_has_many(association_name)
|
89
|
+
# Return a QuerySet with a direct relationship to many
|
90
|
+
# @param [String, Symbol] association_name Association name that identifies a relationship with other objects.
|
91
|
+
# @return [QuerySet, nil] QuerySet based on the association_name, nil if the relationship is not found.
|
92
|
+
def _objects_direct_has_many(association_name)
|
93
|
+
association = self.class.reflect_on_association(association_name.to_sym)
|
94
|
+
return nil unless association
|
95
|
+
target = Object.const_get(association.class_name)
|
96
|
+
begin
|
97
|
+
inverse_relationship = association.options.fetch(:inverse_of)
|
98
|
+
rescue KeyError => _exception
|
99
|
+
raise "Relationship #{association.name} of model #{self.class} has no inverse_of option."
|
100
|
+
end
|
101
|
+
target.objects.filter("#{inverse_relationship}#{Babik::Selection::Config::RELATIONSHIP_SEPARATOR}id": self.id)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# All class methods that are injected to ActiveRecord models
|
106
|
+
module ClassMethods
|
107
|
+
|
108
|
+
# @!method objects
|
109
|
+
# QuerySet for the current model.
|
110
|
+
# @return [QuerySet] queryset for the current model.
|
111
|
+
def objects
|
112
|
+
Babik::QuerySet::Base.new(self)
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
|
119
|
+
|
120
|
+
# Include mixin into parent of all active record models (ActiveRecord::Base)
|
121
|
+
ActiveRecord::Base.send(:include, Babik)
|
122
|
+
ActiveRecord::Base.send(:include, ActiveModel::AttributeAssignment)
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Babik
|
4
|
+
# Database configuration
|
5
|
+
class Database
|
6
|
+
# Return database configuration
|
7
|
+
# @return [Hash{adapter:}] Database configuration as a Hash.
|
8
|
+
def self.config
|
9
|
+
ActiveRecord::Base.connection_config
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.escape(string)
|
13
|
+
ActiveRecord::Base.connection.quote(string)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'babik/queryset/mixins/aggregatable'
|
4
|
+
require 'babik/queryset/mixins/bounded'
|
5
|
+
require 'babik/queryset/mixins/clonable'
|
6
|
+
require 'babik/queryset/mixins/countable'
|
7
|
+
require 'babik/queryset/mixins/deletable'
|
8
|
+
require 'babik/queryset/mixins/distinguishable'
|
9
|
+
require 'babik/queryset/mixins/none'
|
10
|
+
require 'babik/queryset/mixins/filterable'
|
11
|
+
require 'babik/queryset/mixins/limitable'
|
12
|
+
require 'babik/queryset/mixins/lockable'
|
13
|
+
require 'babik/queryset/mixins/projectable'
|
14
|
+
require 'babik/queryset/mixins/related_selector'
|
15
|
+
require 'babik/queryset/mixins/set_operations'
|
16
|
+
require 'babik/queryset/mixins/sql_renderizable'
|
17
|
+
require 'babik/queryset/mixins/sortable'
|
18
|
+
require 'babik/queryset/mixins/updatable'
|
19
|
+
|
20
|
+
require 'babik/queryset/components/aggregation'
|
21
|
+
require 'babik/queryset/components/limit'
|
22
|
+
require 'babik/queryset/components/order'
|
23
|
+
require 'babik/queryset/components/projection'
|
24
|
+
require 'babik/queryset/components/select_related'
|
25
|
+
require 'babik/queryset/components/sql_renderer'
|
26
|
+
require 'babik/queryset/components/where'
|
27
|
+
|
28
|
+
require 'babik/queryset/lib/condition'
|
29
|
+
require 'babik/queryset/lib/selection/config'
|
30
|
+
require 'babik/queryset/lib/selection/local_selection'
|
31
|
+
require 'babik/queryset/lib/selection/foreign_selection'
|
32
|
+
require 'babik/queryset/lib/field'
|
33
|
+
require 'babik/queryset/lib/update/assignment'
|
34
|
+
|
35
|
+
# Represents a new type of query result set
|
36
|
+
module Babik
|
37
|
+
module QuerySet
|
38
|
+
# Abstract Base class for QuerySet, implements a container for database results.
|
39
|
+
class AbstractBase
|
40
|
+
include Enumerable
|
41
|
+
include Babik::QuerySet::Aggregatable
|
42
|
+
include Babik::QuerySet::Bounded
|
43
|
+
include Babik::QuerySet::Clonable
|
44
|
+
include Babik::QuerySet::Countable
|
45
|
+
include Babik::QuerySet::Deletable
|
46
|
+
include Babik::QuerySet::NoneQuerySet
|
47
|
+
include Babik::QuerySet::Distinguishable
|
48
|
+
include Babik::QuerySet::Filterable
|
49
|
+
include Babik::QuerySet::Limitable
|
50
|
+
include Babik::QuerySet::Lockable
|
51
|
+
include Babik::QuerySet::Projectable
|
52
|
+
include Babik::QuerySet::SQLRenderizable
|
53
|
+
include Babik::QuerySet::RelatedSelector
|
54
|
+
include Babik::QuerySet::SetOperations
|
55
|
+
include Babik::QuerySet::Sortable
|
56
|
+
include Babik::QuerySet::Updatable
|
57
|
+
|
58
|
+
attr_reader :model, :_aggregation, :_count, :_distinct, :_limit, :_lock_type, :_order, :_projection,
|
59
|
+
:_where, :_select_related
|
60
|
+
|
61
|
+
alias aggregation? _aggregation
|
62
|
+
alias count? _count
|
63
|
+
alias distinct? _distinct
|
64
|
+
alias select_related? _select_related
|
65
|
+
alias reverse! invert_order!
|
66
|
+
alias select_for_update! for_update!
|
67
|
+
alias exist? exists?
|
68
|
+
|
69
|
+
def initialize(model_class)
|
70
|
+
@model = model_class
|
71
|
+
@_count = false
|
72
|
+
@_distinct = false
|
73
|
+
@_order = nil
|
74
|
+
@_lock_type = nil
|
75
|
+
@_where = Babik::QuerySet::Where.new(@model)
|
76
|
+
@_aggregation = nil
|
77
|
+
@_limit = nil
|
78
|
+
@_projection = nil
|
79
|
+
@_select_related = nil
|
80
|
+
end
|
81
|
+
|
82
|
+
# Return a ResultSet with the ActiveRecord objects that match the condition given by the filters.
|
83
|
+
# @return [ResultSet] ActiveRecord objects that match the condition given by the filters.
|
84
|
+
def all
|
85
|
+
sql_select = self.sql.select
|
86
|
+
return @_projection.apply_transforms(self.class._execute_sql(sql_select)) if @_projection
|
87
|
+
return @_select_related.all_with_related(self.class._execute_sql(sql_select)) if @_select_related
|
88
|
+
@model.find_by_sql(sql_select)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Loop through the results with a block
|
92
|
+
# @param block [Proc] Proc that will be applied to each object.
|
93
|
+
def each(&block)
|
94
|
+
self.all.each(&block)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Get the left joins grouped by alias in a hash.
|
98
|
+
# @return [Hash] Return a hash with the format :table_alias => SQL::Join
|
99
|
+
def left_joins_by_alias
|
100
|
+
left_joins_by_alias = {}
|
101
|
+
# Merge where
|
102
|
+
left_joins_by_alias.merge!(@_where.left_joins_by_alias)
|
103
|
+
# Merge order
|
104
|
+
left_joins_by_alias.merge!(@_order.left_joins_by_alias) if @_order
|
105
|
+
# Merge aggregation
|
106
|
+
left_joins_by_alias.merge!(@_aggregation.left_joins_by_alias) if @_aggregation
|
107
|
+
# Merge prefetchs
|
108
|
+
left_joins_by_alias.merge!(@_select_related.left_joins_by_alias) if @_select_related
|
109
|
+
# Return the left joins by alias
|
110
|
+
left_joins_by_alias
|
111
|
+
end
|
112
|
+
|
113
|
+
# Execute SQL code
|
114
|
+
# @param [String] sql SQL code
|
115
|
+
# @return SQL result set.
|
116
|
+
def self._execute_sql(sql)
|
117
|
+
ActiveRecord::Base.connection.exec_query(sql)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
class Base < AbstractBase
|
122
|
+
|
123
|
+
end
|
124
|
+
|
125
|
+
# Each one of the set operations that can be executed in SQL
|
126
|
+
class SetOperation < AbstractBase
|
127
|
+
|
128
|
+
attr_reader :left_operand, :right_operand
|
129
|
+
|
130
|
+
def initialize(model, left_operand, right_operand)
|
131
|
+
@left_operand = left_operand
|
132
|
+
@right_operand = right_operand
|
133
|
+
super(model)
|
134
|
+
end
|
135
|
+
|
136
|
+
def operation
|
137
|
+
db_adapter = Babik::Database.config[:adapter]
|
138
|
+
operation_name = self.class.to_s.split('::').last.upcase
|
139
|
+
if %w[postgresql sqlite3].include?(db_adapter) || (%w[mysql2].include?(db_adapter) && operation_name == 'UNION')
|
140
|
+
return operation_name
|
141
|
+
end
|
142
|
+
raise "#{db_adapter} does not support operation #{operation_name}"
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
|
147
|
+
class Except < SetOperation; end
|
148
|
+
|
149
|
+
class Intersect < SetOperation; end
|
150
|
+
|
151
|
+
class Union < SetOperation; end
|
152
|
+
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Common module for Babik library
|
4
|
+
module Babik
|
5
|
+
# QuerySet module
|
6
|
+
module QuerySet
|
7
|
+
# A set of aggregation operations
|
8
|
+
class Aggregation
|
9
|
+
|
10
|
+
attr_reader :model, :functions
|
11
|
+
|
12
|
+
# Construct a new aggregation
|
13
|
+
# @param model [Class] class that inherits from ActiveRecord::Base.
|
14
|
+
# @param functions [Array<Avg, Max, Min, Sum>] array of aggregation functions.
|
15
|
+
def initialize(model, functions)
|
16
|
+
@model = model
|
17
|
+
@functions = []
|
18
|
+
functions.each do |field_name, function|
|
19
|
+
@functions << function.prepare(@model, field_name)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Return the joins grouped by alias
|
24
|
+
# @return [Hash{alias: Babik::QuerySet::Join}] Hash where the value is the alias of the table and the value is a Babik::Join
|
25
|
+
def left_joins_by_alias
|
26
|
+
left_joins_by_alias = {}
|
27
|
+
@functions.each do |function|
|
28
|
+
left_joins_by_alias.merge!(function.left_joins_by_alias)
|
29
|
+
end
|
30
|
+
left_joins_by_alias
|
31
|
+
end
|
32
|
+
|
33
|
+
# Return aggregation SQL
|
34
|
+
# @return [String] Aggregation SQL
|
35
|
+
def sql
|
36
|
+
@functions.map(&:sql).join(', ')
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Abstract aggregation function. Do not use
|
41
|
+
class AbstractAggregationFunction
|
42
|
+
attr_reader :model, :selection, :field_name
|
43
|
+
|
44
|
+
# Construct a aggregation function for a field
|
45
|
+
# @param aggregation_path [String] Field or foreign field path.
|
46
|
+
def initialize(aggregation_path)
|
47
|
+
@aggregation_path = aggregation_path
|
48
|
+
end
|
49
|
+
|
50
|
+
# Prepare the aggregation function for a model class and a field
|
51
|
+
# @param model [ActiveRecord::Base] model that will be used as origin for association paths.
|
52
|
+
# @param field_name [String, nil] Name that will take the computed aggregation operation.
|
53
|
+
# If nil, it will take the value <table_alias>__<agg_function>.
|
54
|
+
def prepare(model, field_name = nil)
|
55
|
+
@model = model
|
56
|
+
@selection = Babik::Selection::Path::Factory.build(model, @aggregation_path)
|
57
|
+
@field_name = field_name || "#{self.table_alias}__#{SQL_OPERATION.downcase}"
|
58
|
+
self
|
59
|
+
end
|
60
|
+
|
61
|
+
# Return aggregation function SQL
|
62
|
+
# @return [String] Aggregation function SQL
|
63
|
+
def sql
|
64
|
+
selected_field_path = "#{@selection.target_alias}.#{@selection.selected_field}"
|
65
|
+
operation = self.sql_operation.sub('?field', selected_field_path)
|
66
|
+
"#{operation} AS #{@field_name}"
|
67
|
+
end
|
68
|
+
|
69
|
+
# Return the joins grouped by alias
|
70
|
+
# @return [Hash{alias: Babik::QuerySet::Join}] Hash where the value is the alias of the table and the value is a Babik::Join
|
71
|
+
def left_joins_by_alias
|
72
|
+
@selection.left_joins_by_alias
|
73
|
+
end
|
74
|
+
|
75
|
+
# Return the database adapter
|
76
|
+
# @return [String] Database adapter: 'mysql2', 'postgres' o 'sqlite'
|
77
|
+
def self.db_adapter
|
78
|
+
Babik::Database.config[:adapter]
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
# Class method utility method
|
84
|
+
# @param operation [String] Function that will be executed in the aggregation.
|
85
|
+
# @param aggregation_path [String]
|
86
|
+
# @return [Class < AbstractAggregationFunction] aggregation function object.
|
87
|
+
def self.agg(operation, aggregation_path)
|
88
|
+
operation_class_name = operation.to_s.camelize
|
89
|
+
operation_class = Object.const_get("Babik::QuerySet::#{operation_class_name}")
|
90
|
+
operation_class.new(aggregation_path)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Mixin that injects the sql_operation method in aggregations with the same SQL syntax
|
94
|
+
# independently of the database adapter (SUM, MAX, MIN, etc.)
|
95
|
+
module StandardSqlOperation
|
96
|
+
def sql_operation
|
97
|
+
self.class::SQL_OPERATION
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Average operation. Compute the mean of a set of values.
|
102
|
+
class Avg < AbstractAggregationFunction
|
103
|
+
include StandardSqlOperation
|
104
|
+
SQL_OPERATION = 'AVG(?field)'
|
105
|
+
end
|
106
|
+
|
107
|
+
# Count operation. Compute the count of a set of values.
|
108
|
+
class Count < AbstractAggregationFunction
|
109
|
+
include StandardSqlOperation
|
110
|
+
SQL_OPERATION = 'COUNT(?field)'
|
111
|
+
end
|
112
|
+
|
113
|
+
# Count distinct operation. Compute the count distinct of a set of values.
|
114
|
+
class CountDistinct < AbstractAggregationFunction
|
115
|
+
include StandardSqlOperation
|
116
|
+
SQL_OPERATION = 'COUNT(DISTINCT(?field))'
|
117
|
+
end
|
118
|
+
|
119
|
+
# Max operation. Compute the maximum of a set of values.
|
120
|
+
class Max < AbstractAggregationFunction
|
121
|
+
include StandardSqlOperation
|
122
|
+
SQL_OPERATION = 'MAX(?field)'
|
123
|
+
end
|
124
|
+
|
125
|
+
# Min operation. Compute the minimum of a set of values.
|
126
|
+
class Min < AbstractAggregationFunction
|
127
|
+
include StandardSqlOperation
|
128
|
+
SQL_OPERATION = 'MIN(?field)'
|
129
|
+
end
|
130
|
+
|
131
|
+
# Sum operation. Compute the sum of a set of values.
|
132
|
+
class Sum < AbstractAggregationFunction
|
133
|
+
include StandardSqlOperation
|
134
|
+
SQL_OPERATION = 'SUM(?field)'
|
135
|
+
end
|
136
|
+
|
137
|
+
# When a aggregation function is in PostgreSQL and MySQL (main supported databases)
|
138
|
+
class PostgresMySQLAggregationFunction < AbstractAggregationFunction
|
139
|
+
# Return the SQL code operation for this aggregation, e.g.:
|
140
|
+
# - STDDEV_POP(?field)
|
141
|
+
# - VAR_POP(?field)
|
142
|
+
# @raise [RuntimeException] if database has no support for this operation.
|
143
|
+
# @return [String] SQL code for the aggregation
|
144
|
+
def sql_operation
|
145
|
+
db_adapter = self.class.db_adapter
|
146
|
+
return self.class::SQL_OPERATION if %w[postgresql mysql2].include?(db_adapter)
|
147
|
+
raise "#{db_adapter} has no support for #{self.class} aggregation"
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# Standard deviation of a set of values
|
152
|
+
class StdDev < PostgresMySQLAggregationFunction
|
153
|
+
SQL_OPERATION = 'STDDEV_POP(?field)'
|
154
|
+
end
|
155
|
+
|
156
|
+
# Standard deviation (sample) of a set of values
|
157
|
+
class StdDevSample < PostgresMySQLAggregationFunction
|
158
|
+
SQL_OPERATION = 'STDDEV_SAMP(?field)'
|
159
|
+
end
|
160
|
+
|
161
|
+
# Variance of a set of values
|
162
|
+
class Var < PostgresMySQLAggregationFunction
|
163
|
+
SQL_OPERATION = 'VAR_POP(?field)'
|
164
|
+
end
|
165
|
+
|
166
|
+
# Variance (sample) of a set of values
|
167
|
+
class VarSample < PostgresMySQLAggregationFunction
|
168
|
+
SQL_OPERATION = 'VAR_SAMP(?field)'
|
169
|
+
end
|
170
|
+
|
171
|
+
end
|
172
|
+
end
|