switchman 2.0.13 → 3.0.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 +4 -4
- data/Rakefile +10 -2
- data/app/models/switchman/shard.rb +234 -271
- data/app/models/switchman/unsharded_record.rb +7 -0
- data/db/migrate/20130328212039_create_switchman_shards.rb +1 -1
- data/db/migrate/20130328224244_create_default_shard.rb +5 -5
- data/db/migrate/20161206323434_add_back_default_string_limits_switchman.rb +1 -0
- data/db/migrate/20180828183945_add_default_shard_index.rb +2 -2
- data/db/migrate/20180828192111_add_timestamps_to_shards.rb +7 -5
- data/db/migrate/20190114212900_add_unique_name_indexes.rb +5 -3
- data/lib/switchman.rb +3 -5
- data/lib/switchman/action_controller/caching.rb +2 -2
- data/lib/switchman/active_record/abstract_adapter.rb +1 -0
- data/lib/switchman/active_record/association.rb +78 -89
- data/lib/switchman/active_record/attribute_methods.rb +58 -52
- data/lib/switchman/active_record/base.rb +58 -59
- data/lib/switchman/active_record/calculations.rb +74 -67
- data/lib/switchman/active_record/connection_pool.rb +14 -41
- data/lib/switchman/active_record/database_configurations.rb +34 -0
- data/lib/switchman/active_record/database_configurations/database_config.rb +13 -0
- data/lib/switchman/active_record/finder_methods.rb +11 -16
- data/lib/switchman/active_record/log_subscriber.rb +4 -8
- data/lib/switchman/active_record/migration.rb +6 -47
- data/lib/switchman/active_record/model_schema.rb +1 -1
- data/lib/switchman/active_record/persistence.rb +4 -6
- data/lib/switchman/active_record/postgresql_adapter.rb +124 -168
- data/lib/switchman/active_record/predicate_builder.rb +2 -2
- data/lib/switchman/active_record/query_cache.rb +18 -19
- data/lib/switchman/active_record/query_methods.rb +172 -197
- data/lib/switchman/active_record/reflection.rb +6 -10
- data/lib/switchman/active_record/relation.rb +30 -78
- data/lib/switchman/active_record/spawn_methods.rb +27 -29
- data/lib/switchman/active_record/statement_cache.rb +18 -35
- data/lib/switchman/active_record/tasks/database_tasks.rb +16 -0
- data/lib/switchman/active_support/cache.rb +3 -5
- data/lib/switchman/arel.rb +13 -8
- data/lib/switchman/database_server.rb +121 -142
- data/lib/switchman/default_shard.rb +52 -16
- data/lib/switchman/engine.rb +61 -58
- data/lib/switchman/environment.rb +4 -8
- data/lib/switchman/errors.rb +1 -0
- data/lib/switchman/guard_rail.rb +6 -19
- data/lib/switchman/guard_rail/relation.rb +5 -7
- data/lib/switchman/r_spec_helper.rb +29 -37
- data/lib/switchman/rails.rb +14 -12
- data/lib/switchman/schema_cache.rb +1 -9
- data/lib/switchman/sharded_instrumenter.rb +1 -1
- data/lib/switchman/standard_error.rb +15 -3
- data/lib/switchman/test_helper.rb +7 -11
- data/lib/switchman/version.rb +1 -1
- data/lib/tasks/switchman.rake +54 -69
- metadata +87 -45
- data/lib/switchman/active_record/batches.rb +0 -11
- data/lib/switchman/active_record/connection_handler.rb +0 -172
- data/lib/switchman/active_record/where_clause_factory.rb +0 -36
- data/lib/switchman/connection_pool_proxy.rb +0 -173
@@ -6,49 +6,40 @@ module Switchman
|
|
6
6
|
module ClassMethods
|
7
7
|
delegate :shard, to: :all
|
8
8
|
|
9
|
-
def find_ids_in_ranges(opts={}, &block)
|
10
|
-
opts.reverse_merge!(:
|
9
|
+
def find_ids_in_ranges(opts = {}, &block)
|
10
|
+
opts.reverse_merge!(loose: true)
|
11
11
|
all.find_ids_in_ranges(opts, &block)
|
12
12
|
end
|
13
13
|
|
14
|
-
def
|
15
|
-
|
16
|
-
end
|
14
|
+
def sharded_model
|
15
|
+
self.abstract_class = true
|
17
16
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
categories[shard_category].delete(self)
|
22
|
-
categories.delete(shard_category) if categories[shard_category].empty?
|
23
|
-
end
|
24
|
-
categories[category] ||= []
|
25
|
-
categories[category] << self
|
26
|
-
self.connection_specification_name = category.to_s
|
17
|
+
return if self == UnshardedRecord
|
18
|
+
|
19
|
+
Shard.send(:add_sharded_model, self)
|
27
20
|
end
|
28
21
|
|
29
22
|
def integral_id?
|
30
|
-
|
31
|
-
@integral_id = columns_hash[primary_key]&.type == :integer
|
32
|
-
end
|
23
|
+
@integral_id = columns_hash[primary_key]&.type == :integer if @integral_id.nil?
|
33
24
|
@integral_id
|
34
25
|
end
|
35
26
|
|
36
27
|
def transaction(**)
|
37
28
|
if self != ::ActiveRecord::Base && current_scope
|
38
29
|
current_scope.activate do
|
39
|
-
db = Shard.current(
|
40
|
-
if ::GuardRail.environment
|
41
|
-
db.unguard { super }
|
42
|
-
else
|
30
|
+
db = Shard.current(connection_classes).database_server
|
31
|
+
if ::GuardRail.environment == db.guard_rail_environment
|
43
32
|
super
|
33
|
+
else
|
34
|
+
db.unguard { super }
|
44
35
|
end
|
45
36
|
end
|
46
37
|
else
|
47
|
-
db = Shard.current(
|
48
|
-
if ::GuardRail.environment
|
49
|
-
db.unguard { super }
|
50
|
-
else
|
38
|
+
db = Shard.current(connection_classes).database_server
|
39
|
+
if ::GuardRail.environment == db.guard_rail_environment
|
51
40
|
super
|
41
|
+
else
|
42
|
+
db.unguard { super }
|
52
43
|
end
|
53
44
|
end
|
54
45
|
end
|
@@ -72,39 +63,46 @@ module Switchman
|
|
72
63
|
end
|
73
64
|
|
74
65
|
def clear_query_caches_for_current_thread
|
75
|
-
::ActiveRecord::Base.
|
76
|
-
|
77
|
-
pool.connection(switch_shard: false).clear_query_cache if pool.active_connection?
|
78
|
-
end
|
66
|
+
::ActiveRecord::Base.connection_handler.connection_pool_list.each do |pool|
|
67
|
+
pool.connection(switch_shard: false).clear_query_cache if pool.active_connection?
|
79
68
|
end
|
80
69
|
end
|
70
|
+
|
71
|
+
# significant change: _don't_ check if klasses.include?(Base)
|
72
|
+
# i.e. other sharded models don't inherit the current shard of Base
|
73
|
+
def current_shard
|
74
|
+
connected_to_stack.reverse_each do |hash|
|
75
|
+
return hash[:shard] if hash[:shard] && hash[:klasses].include?(connection_classes)
|
76
|
+
end
|
77
|
+
|
78
|
+
default_shard
|
79
|
+
end
|
81
80
|
end
|
82
81
|
|
83
82
|
def self.included(klass)
|
84
|
-
klass.
|
83
|
+
klass.singleton_class.prepend(ClassMethods)
|
85
84
|
klass.set_callback(:initialize, :before) do
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
end
|
92
|
-
end
|
85
|
+
@shard ||= if self.class.sharded_primary_key?
|
86
|
+
Shard.shard_for(self[self.class.primary_key], Shard.current(self.class.connection_classes))
|
87
|
+
else
|
88
|
+
Shard.current(self.class.connection_classes)
|
89
|
+
end
|
93
90
|
end
|
94
91
|
end
|
95
92
|
|
96
93
|
def shard
|
97
|
-
@shard || Shard.current(self.class.
|
94
|
+
@shard || Shard.current(self.class.connection_classes) || Shard.default
|
98
95
|
end
|
99
96
|
|
100
97
|
def shard=(new_shard)
|
101
|
-
raise ::ActiveRecord::ReadOnlyRecord if !
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
98
|
+
raise ::ActiveRecord::ReadOnlyRecord if !new_record? || @shard_set_in_stone
|
99
|
+
|
100
|
+
return if shard == new_shard
|
101
|
+
|
102
|
+
attributes.each do |attr, value|
|
103
|
+
self[attr] = Shard.relative_id_for(value, shard, new_shard) if self.class.sharded_column?(attr)
|
107
104
|
end
|
105
|
+
@shard = new_shard
|
108
106
|
end
|
109
107
|
|
110
108
|
def save(*, **)
|
@@ -118,7 +116,7 @@ module Switchman
|
|
118
116
|
end
|
119
117
|
|
120
118
|
def destroy
|
121
|
-
self.class.
|
119
|
+
shard.activate(self.class.connection_classes) { super }
|
122
120
|
end
|
123
121
|
|
124
122
|
def clone
|
@@ -126,23 +124,23 @@ module Switchman
|
|
126
124
|
# TODO: adjust foreign keys
|
127
125
|
# don't use the setter, cause the foreign keys are already
|
128
126
|
# relative to this shard
|
129
|
-
result.instance_variable_set(:@shard,
|
127
|
+
result.instance_variable_set(:@shard, shard)
|
130
128
|
result
|
131
129
|
end
|
132
130
|
|
133
131
|
def transaction(**kwargs, &block)
|
134
|
-
shard.activate(self.class.
|
132
|
+
shard.activate(self.class.connection_classes) do
|
135
133
|
self.class.transaction(**kwargs, &block)
|
136
134
|
end
|
137
135
|
end
|
138
136
|
|
139
137
|
def hash
|
140
|
-
self.class.sharded_primary_key? ? self.class.hash ^
|
138
|
+
self.class.sharded_primary_key? ? self.class.hash ^ global_id.hash : super
|
141
139
|
end
|
142
140
|
|
143
141
|
def to_param
|
144
142
|
short_id = Shard.short_id_for(id)
|
145
|
-
short_id
|
143
|
+
short_id&.to_s
|
146
144
|
end
|
147
145
|
|
148
146
|
def initialize_dup(*args)
|
@@ -153,37 +151,38 @@ module Switchman
|
|
153
151
|
|
154
152
|
def quoted_id
|
155
153
|
return super unless self.class.sharded_primary_key?
|
154
|
+
|
156
155
|
# do this the Rails 4.2 way, so that if Shard.current != self.shard, the id gets transposed
|
157
156
|
self.class.connection.quote(id)
|
158
157
|
end
|
159
158
|
|
160
159
|
def update_columns(*)
|
161
|
-
db = Shard.current(self.class.
|
162
|
-
if ::GuardRail.environment
|
163
|
-
return db.unguard { super }
|
164
|
-
else
|
160
|
+
db = Shard.current(self.class.connection_classes).database_server
|
161
|
+
if ::GuardRail.environment == db.guard_rail_environment
|
165
162
|
super
|
163
|
+
else
|
164
|
+
db.unguard { super }
|
166
165
|
end
|
167
166
|
end
|
168
167
|
|
169
168
|
protected
|
170
169
|
|
171
|
-
# see also AttributeMethods#
|
172
|
-
def
|
170
|
+
# see also AttributeMethods#connection_classes_code_for_reflection
|
171
|
+
def connection_classes_for_reflection(reflection)
|
173
172
|
if reflection
|
174
173
|
if reflection.options[:polymorphic]
|
175
174
|
begin
|
176
|
-
read_attribute(reflection.foreign_type)&.constantize&.
|
175
|
+
read_attribute(reflection.foreign_type)&.constantize&.connection_classes
|
177
176
|
rescue NameError
|
178
177
|
# in case someone is abusing foreign_type to not point to an actual class
|
179
|
-
|
178
|
+
::ActiveRecord::Base
|
180
179
|
end
|
181
180
|
else
|
182
181
|
# otherwise we can just return a symbol for the statically known type of the association
|
183
|
-
reflection.klass.
|
182
|
+
reflection.klass.connection_classes
|
184
183
|
end
|
185
184
|
else
|
186
|
-
|
185
|
+
connection_classes
|
187
186
|
end
|
188
187
|
end
|
189
188
|
end
|
@@ -3,45 +3,44 @@
|
|
3
3
|
module Switchman
|
4
4
|
module ActiveRecord
|
5
5
|
module Calculations
|
6
|
-
|
7
6
|
def pluck(*column_names)
|
8
|
-
target_shard = Shard.current(klass.
|
7
|
+
target_shard = Shard.current(klass.connection_classes)
|
9
8
|
shard_count = 0
|
10
|
-
result =
|
9
|
+
result = activate do |relation, shard|
|
11
10
|
shard_count += 1
|
12
11
|
results = relation.call_super(:pluck, Calculations, *column_names)
|
13
12
|
if column_names.length > 1
|
14
13
|
column_names.each_with_index do |column_name, idx|
|
15
|
-
|
16
|
-
|
14
|
+
next unless klass.sharded_column?(column_name)
|
15
|
+
|
16
|
+
results.each do |r|
|
17
|
+
r[idx] = Shard.relative_id_for(r[idx], shard, target_shard)
|
17
18
|
end
|
18
19
|
end
|
19
|
-
|
20
|
-
|
21
|
-
results = results.map{|result| Shard.relative_id_for(result, shard, target_shard)}
|
22
|
-
end
|
20
|
+
elsif klass.sharded_column?(column_names.first.to_s)
|
21
|
+
results = results.map { |r| Shard.relative_id_for(r, shard, target_shard) }
|
23
22
|
end
|
24
23
|
results
|
25
24
|
end
|
26
|
-
if distinct_value && shard_count > 1
|
27
|
-
result.uniq!
|
28
|
-
end
|
25
|
+
result.uniq! if distinct_value && shard_count > 1
|
29
26
|
result
|
30
27
|
end
|
31
28
|
|
32
29
|
def execute_simple_calculation(operation, column_name, distinct)
|
33
30
|
operation = operation.to_s.downcase
|
34
|
-
if operation ==
|
31
|
+
if operation == 'average'
|
35
32
|
result = calculate_simple_average(column_name, distinct)
|
36
33
|
else
|
37
|
-
result =
|
34
|
+
result = activate do |relation|
|
35
|
+
relation.call_super(:execute_simple_calculation, Calculations, operation, column_name, distinct)
|
36
|
+
end
|
38
37
|
if result.is_a?(Array)
|
39
38
|
case operation
|
40
|
-
when
|
39
|
+
when 'count', 'sum'
|
41
40
|
result = result.sum
|
42
|
-
when
|
41
|
+
when 'minimum'
|
43
42
|
result = result.min
|
44
|
-
when
|
43
|
+
when 'maximum'
|
45
44
|
result = result.max
|
46
45
|
end
|
47
46
|
end
|
@@ -51,20 +50,22 @@ module Switchman
|
|
51
50
|
|
52
51
|
def calculate_simple_average(column_name, distinct)
|
53
52
|
# See activerecord#execute_simple_calculation
|
54
|
-
relation =
|
53
|
+
relation = reorder(nil)
|
55
54
|
column = aggregate_column(column_name)
|
56
|
-
relation.select_values = [operation_over_aggregate_column(column,
|
57
|
-
operation_over_aggregate_column(column,
|
55
|
+
relation.select_values = [operation_over_aggregate_column(column, 'average', distinct).as('average'),
|
56
|
+
operation_over_aggregate_column(column, 'count', distinct).as('count')]
|
58
57
|
|
59
|
-
initial_results = relation.activate{ |rel| klass.connection.select_all(rel) }
|
58
|
+
initial_results = relation.activate { |rel| klass.connection.select_all(rel) }
|
60
59
|
if initial_results.is_a?(Array)
|
61
60
|
initial_results.each do |r|
|
62
|
-
r[
|
63
|
-
r[
|
61
|
+
r['average'] = type_cast_calculated_value_switchman(r['average'], column_name, 'average')
|
62
|
+
r['count'] = type_cast_calculated_value_switchman(r['count'], column_name, 'count')
|
64
63
|
end
|
65
|
-
result = initial_results.map{|r| r[
|
64
|
+
result = initial_results.map { |r| r['average'] * r['count'] }.sum / initial_results.map do |r|
|
65
|
+
r['count']
|
66
|
+
end.sum
|
66
67
|
else
|
67
|
-
result =
|
68
|
+
result = type_cast_calculated_value_switchman(initial_results.first['average'], column_name, 'average')
|
68
69
|
end
|
69
70
|
result
|
70
71
|
end
|
@@ -74,27 +75,28 @@ module Switchman
|
|
74
75
|
opts = grouped_calculation_options(operation.to_s.downcase, column_name, distinct)
|
75
76
|
|
76
77
|
relation = build_grouped_calculation_relation(opts)
|
77
|
-
target_shard = Shard.current
|
78
|
+
target_shard = Shard.current
|
78
79
|
|
79
80
|
rows = relation.activate do |rel, shard|
|
80
81
|
calculated_data = klass.connection.select_all(rel)
|
81
82
|
|
82
83
|
if opts[:association]
|
83
84
|
key_ids = calculated_data.collect { |row| row[opts[:group_aliases].first] }
|
84
|
-
key_records = opts[:association].klass.base_class.where(:
|
85
|
-
key_records =
|
85
|
+
key_records = opts[:association].klass.base_class.where(id: key_ids)
|
86
|
+
key_records = key_records.map { |r| [Shard.relative_id_for(r, shard, target_shard), r] }.to_h
|
86
87
|
end
|
87
88
|
|
88
89
|
calculated_data.map do |row|
|
89
|
-
row[opts[:aggregate_alias]] =
|
90
|
-
|
90
|
+
row[opts[:aggregate_alias]] = type_cast_calculated_value_switchman(
|
91
|
+
row[opts[:aggregate_alias]], column_name, opts[:operation]
|
92
|
+
)
|
91
93
|
row['count'] = row['count'].to_i if opts[:operation] == 'average'
|
92
94
|
|
93
|
-
opts[:group_columns].each do |aliaz,
|
95
|
+
opts[:group_columns].each do |aliaz, _type, group_column_name|
|
94
96
|
if opts[:associated] && (aliaz == opts[:group_aliases].first)
|
95
97
|
row[aliaz] = key_records[Shard.relative_id_for(row[aliaz], shard, target_shard)]
|
96
|
-
elsif
|
97
|
-
row[aliaz] = Shard.relative_id_for(
|
98
|
+
elsif group_column_name && @klass.sharded_column?(group_column_name)
|
99
|
+
row[aliaz] = Shard.relative_id_for(row[aliaz], shard, target_shard)
|
98
100
|
end
|
99
101
|
end
|
100
102
|
row
|
@@ -106,12 +108,21 @@ module Switchman
|
|
106
108
|
|
107
109
|
private
|
108
110
|
|
111
|
+
def type_cast_calculated_value_switchman(value, column_name, operation)
|
112
|
+
type_cast_calculated_value(value, operation) do |val|
|
113
|
+
column = aggregate_column(column_name)
|
114
|
+
type ||= column.try(:type_caster) ||
|
115
|
+
lookup_cast_type_from_join_dependencies(column_name.to_s) || Type.default_value
|
116
|
+
type.deserialize(val)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
109
120
|
def column_name_for(field)
|
110
121
|
field.respond_to?(:name) ? field.name.to_s : field.to_s.split('.').last
|
111
122
|
end
|
112
123
|
|
113
124
|
def grouped_calculation_options(operation, column_name, distinct)
|
114
|
-
opts = {:
|
125
|
+
opts = { operation: operation, column_name: column_name, distinct: distinct }
|
115
126
|
|
116
127
|
opts[:aggregate_alias] = aggregate_alias_for(operation, column_name)
|
117
128
|
group_attrs = group_values
|
@@ -125,12 +136,12 @@ module Switchman
|
|
125
136
|
|
126
137
|
# to_s is because Rails 5 returns a string but Rails 6 returns a symbol.
|
127
138
|
group_aliases = group_fields.map { |field| column_alias_for(field.downcase.to_s).to_s }
|
128
|
-
group_columns = group_aliases.zip(group_fields).map
|
139
|
+
group_columns = group_aliases.zip(group_fields).map do |aliaz, field|
|
129
140
|
[aliaz, type_for(field), column_name_for(field)]
|
130
|
-
|
131
|
-
opts.merge!(:
|
132
|
-
|
133
|
-
|
141
|
+
end
|
142
|
+
opts.merge!(association: association, associated: associated,
|
143
|
+
group_aliases: group_aliases, group_columns: group_columns,
|
144
|
+
group_fields: group_fields)
|
134
145
|
|
135
146
|
opts
|
136
147
|
end
|
@@ -149,28 +160,30 @@ module Switchman
|
|
149
160
|
group = opts[:group_fields]
|
150
161
|
|
151
162
|
select_values = [
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
163
|
+
operation_over_aggregate_column(
|
164
|
+
aggregate_column(opts[:column_name]),
|
165
|
+
opts[:operation],
|
166
|
+
opts[:distinct]
|
167
|
+
).as(opts[:aggregate_alias])
|
156
168
|
]
|
157
|
-
if opts[:operation
|
169
|
+
if opts[:operation] == 'average'
|
158
170
|
# include count in average so we can recalculate the average
|
159
171
|
# across all shards if needed
|
160
172
|
select_values << operation_over_aggregate_column(
|
161
|
-
|
162
|
-
|
173
|
+
aggregate_column(opts[:column_name]),
|
174
|
+
'count', opts[:distinct]
|
175
|
+
).as('count')
|
163
176
|
end
|
164
177
|
|
165
178
|
haves = having_clause.send(:predicates)
|
166
179
|
select_values += select_values unless haves.empty?
|
167
|
-
select_values.concat
|
180
|
+
select_values.concat(opts[:group_fields].zip(opts[:group_aliases]).map do |field, aliaz|
|
168
181
|
if field.respond_to?(:as)
|
169
182
|
field.as(aliaz)
|
170
183
|
else
|
171
184
|
"#{field} AS #{aliaz}"
|
172
185
|
end
|
173
|
-
|
186
|
+
end)
|
174
187
|
|
175
188
|
relation = except(:group)
|
176
189
|
relation.group_values = group
|
@@ -181,12 +194,12 @@ module Switchman
|
|
181
194
|
def compact_grouped_calculation_rows(rows, opts)
|
182
195
|
result = ::ActiveSupport::OrderedHash.new
|
183
196
|
rows.each do |row|
|
184
|
-
key = opts[:group_columns].map { |aliaz,
|
197
|
+
key = opts[:group_columns].map { |aliaz, _column| row[aliaz] }
|
185
198
|
key = key.first if key.size == 1
|
186
199
|
value = row[opts[:aggregate_alias]]
|
187
200
|
|
188
201
|
if opts[:operation] == 'average'
|
189
|
-
if result.
|
202
|
+
if result.key?(key)
|
190
203
|
old_value, old_count = result[key]
|
191
204
|
new_count = old_count + row['count']
|
192
205
|
new_value = ((old_value * old_count) + (value * row['count'])) / new_count
|
@@ -194,30 +207,24 @@ module Switchman
|
|
194
207
|
else
|
195
208
|
result[key] = [value, row['count']]
|
196
209
|
end
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
result[key] = value if value > result[key]
|
206
|
-
end
|
207
|
-
else
|
208
|
-
result[key] = value
|
210
|
+
elsif result.key?(key)
|
211
|
+
case opts[:operation]
|
212
|
+
when 'count', 'sum'
|
213
|
+
result[key] += value
|
214
|
+
when 'minimum'
|
215
|
+
result[key] = value if value < result[key]
|
216
|
+
when 'maximum'
|
217
|
+
result[key] = value if value > result[key]
|
209
218
|
end
|
219
|
+
else
|
220
|
+
result[key] = value
|
210
221
|
end
|
211
222
|
end
|
212
223
|
|
213
|
-
if opts[:operation] == 'average'
|
214
|
-
result = Hash[result.map{|k, v| [k, v.first]}]
|
215
|
-
end
|
224
|
+
result.transform_values!(&:first) if opts[:operation] == 'average'
|
216
225
|
|
217
226
|
result
|
218
227
|
end
|
219
|
-
|
220
|
-
|
221
228
|
end
|
222
229
|
end
|
223
230
|
end
|