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,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
@@ -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