partitioned 1.1.1 → 1.1.3
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.
- data/.rspec +2 -0
- data/Gemfile.lock +116 -0
- data/examples/company_id.rb +1 -1
- data/examples/company_id_and_created_at.rb +1 -1
- data/examples/created_at.rb +1 -1
- data/examples/created_at_referencing_awards.rb +2 -2
- data/examples/id.rb +1 -1
- data/examples/lib/company.rb +1 -1
- data/examples/start_date.rb +1 -1
- data/lib/partitioned/by_yearly_time_field.rb +29 -0
- data/lib/partitioned/partitioned_base.rb +5 -8
- data/lib/partitioned/version.rb +1 -1
- data/lib/partitioned.rb +2 -2
- data/partitioned.gemspec +5 -4
- data/spec/monkey_patch_posgres_spec.rb +1 -1
- data/spec/partitioned/by_yearly_time_field_spec.rb +100 -0
- data/spec/partitioned/multi_level/configurator/dsl_spec.rb +4 -0
- data/spec/partitioned/partitioned_base/configurator/dsl_spec.rb +4 -0
- data/spec/support/shared_example_spec_helper_for_time_key.rb +3 -3
- data/spec/support/tables_spec_helper.rb +1 -1
- metadata +23 -5
- data/lib/partitioned/bulk_methods_mixin.rb +0 -233
- data/spec/partitioned/bulk_methods_mixin_spec.rb +0 -512
@@ -23,9 +23,13 @@ module Partitioned
|
|
23
23
|
{
|
24
24
|
"on_field" => nil,
|
25
25
|
"indexes" => [],
|
26
|
+
"janitorial_archives_needed" => nil,
|
27
|
+
"janitorial_creates_needed" => nil,
|
28
|
+
"janitorial_drops_needed" => nil,
|
26
29
|
"foreign_keys" => [],
|
27
30
|
"last_partitions_order_by_clause" => nil,
|
28
31
|
"schema_name" => nil,
|
32
|
+
"table_alias_name" => nil,
|
29
33
|
"name_prefix" => nil,
|
30
34
|
"base_name" => nil,
|
31
35
|
"part_name" => nil,
|
@@ -121,7 +121,7 @@ shared_examples_for "check that basic operations with postgres works correctly f
|
|
121
121
|
context "when try to create new record outside the range of partitions" do
|
122
122
|
|
123
123
|
it "raises ActiveRecord::StatementInvalid" do
|
124
|
-
lambda { subject.create_many([{ :created_at => DATE_NOW
|
124
|
+
lambda { subject.create_many([{ :created_at => DATE_NOW - 1.year, :company_id => 1 }])
|
125
125
|
}.should raise_error(ActiveRecord::StatementInvalid)
|
126
126
|
end
|
127
127
|
|
@@ -130,7 +130,7 @@ shared_examples_for "check that basic operations with postgres works correctly f
|
|
130
130
|
context "when try to update a record outside the range of partitions" do
|
131
131
|
|
132
132
|
it "raises ActiveRecord::StatementInvalid" do
|
133
|
-
lambda { subject.update(1, :name => 'Kevin', :created_at => DATE_NOW
|
133
|
+
lambda { subject.update(1, :name => 'Kevin', :created_at => DATE_NOW - 1.year)
|
134
134
|
}.should raise_error(ActiveRecord::StatementInvalid)
|
135
135
|
end
|
136
136
|
|
@@ -139,7 +139,7 @@ shared_examples_for "check that basic operations with postgres works correctly f
|
|
139
139
|
context "when try to find a record outside the range of partitions" do
|
140
140
|
|
141
141
|
it "raises ActiveRecord::StatementInvalid" do
|
142
|
-
lambda { subject.from_partition(DATE_NOW
|
142
|
+
lambda { subject.from_partition(DATE_NOW - 1.year).find(1)
|
143
143
|
}.should raise_error(ActiveRecord::StatementInvalid)
|
144
144
|
end
|
145
145
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: partitioned
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.1.
|
4
|
+
version: 1.1.3
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date:
|
13
|
+
date: 2013-10-14 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: pg
|
@@ -60,6 +60,22 @@ dependencies:
|
|
60
60
|
- - ! '>='
|
61
61
|
- !ruby/object:Gem::Version
|
62
62
|
version: '0'
|
63
|
+
- !ruby/object:Gem::Dependency
|
64
|
+
name: bulk_data_methods
|
65
|
+
requirement: !ruby/object:Gem::Requirement
|
66
|
+
none: false
|
67
|
+
requirements:
|
68
|
+
- - '='
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: 1.0.0
|
71
|
+
type: :runtime
|
72
|
+
prerelease: false
|
73
|
+
version_requirements: !ruby/object:Gem::Requirement
|
74
|
+
none: false
|
75
|
+
requirements:
|
76
|
+
- - '='
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: 1.0.0
|
63
79
|
description: A gem providing support for table partitioning in ActiveRecord. Support
|
64
80
|
is only available for postgres databases. Other features include child table management
|
65
81
|
(creation and deletion) and bulk data creating and updating.
|
@@ -68,7 +84,9 @@ executables: []
|
|
68
84
|
extensions: []
|
69
85
|
extra_rdoc_files: []
|
70
86
|
files:
|
87
|
+
- .rspec
|
71
88
|
- Gemfile
|
89
|
+
- Gemfile.lock
|
72
90
|
- LICENSE
|
73
91
|
- PARTITIONING_EXPLAINED.txt
|
74
92
|
- README
|
@@ -90,7 +108,6 @@ files:
|
|
90
108
|
- lib/monkey_patch_postgres.rb
|
91
109
|
- lib/partitioned.rb
|
92
110
|
- lib/partitioned/active_record_overrides.rb
|
93
|
-
- lib/partitioned/bulk_methods_mixin.rb
|
94
111
|
- lib/partitioned/by_created_at.rb
|
95
112
|
- lib/partitioned/by_foreign_key.rb
|
96
113
|
- lib/partitioned/by_id.rb
|
@@ -98,6 +115,7 @@ files:
|
|
98
115
|
- lib/partitioned/by_monthly_time_field.rb
|
99
116
|
- lib/partitioned/by_time_field.rb
|
100
117
|
- lib/partitioned/by_weekly_time_field.rb
|
118
|
+
- lib/partitioned/by_yearly_time_field.rb
|
101
119
|
- lib/partitioned/multi_level.rb
|
102
120
|
- lib/partitioned/multi_level/configurator/data.rb
|
103
121
|
- lib/partitioned/multi_level/configurator/dsl.rb
|
@@ -144,7 +162,6 @@ files:
|
|
144
162
|
- spec/dummy/script/rails
|
145
163
|
- spec/dummy/spec/spec_helper.rb
|
146
164
|
- spec/monkey_patch_posgres_spec.rb
|
147
|
-
- spec/partitioned/bulk_methods_mixin_spec.rb
|
148
165
|
- spec/partitioned/by_created_at_spec.rb
|
149
166
|
- spec/partitioned/by_foreign_key_spec.rb
|
150
167
|
- spec/partitioned/by_id_spec.rb
|
@@ -152,6 +169,7 @@ files:
|
|
152
169
|
- spec/partitioned/by_monthly_time_field_spec.rb
|
153
170
|
- spec/partitioned/by_time_field_spec.rb
|
154
171
|
- spec/partitioned/by_weekly_time_field_spec.rb
|
172
|
+
- spec/partitioned/by_yearly_time_field_spec.rb
|
155
173
|
- spec/partitioned/multi_level/configurator/dsl_spec.rb
|
156
174
|
- spec/partitioned/multi_level/configurator/reader_spec.rb
|
157
175
|
- spec/partitioned/partitioned_base/configurator/dsl_spec.rb
|
@@ -219,7 +237,6 @@ test_files:
|
|
219
237
|
- spec/dummy/script/rails
|
220
238
|
- spec/dummy/spec/spec_helper.rb
|
221
239
|
- spec/monkey_patch_posgres_spec.rb
|
222
|
-
- spec/partitioned/bulk_methods_mixin_spec.rb
|
223
240
|
- spec/partitioned/by_created_at_spec.rb
|
224
241
|
- spec/partitioned/by_foreign_key_spec.rb
|
225
242
|
- spec/partitioned/by_id_spec.rb
|
@@ -227,6 +244,7 @@ test_files:
|
|
227
244
|
- spec/partitioned/by_monthly_time_field_spec.rb
|
228
245
|
- spec/partitioned/by_time_field_spec.rb
|
229
246
|
- spec/partitioned/by_weekly_time_field_spec.rb
|
247
|
+
- spec/partitioned/by_yearly_time_field_spec.rb
|
230
248
|
- spec/partitioned/multi_level/configurator/dsl_spec.rb
|
231
249
|
- spec/partitioned/multi_level/configurator/reader_spec.rb
|
232
250
|
- spec/partitioned/partitioned_base/configurator/dsl_spec.rb
|
@@ -1,233 +0,0 @@
|
|
1
|
-
module Partitioned
|
2
|
-
#
|
3
|
-
# MixIn used to extend ActiveRecord::Base classes implementing bulk insert and update operations
|
4
|
-
# through {#create_many} and {#update_many}. {Partitioned::PartitionedBase} classes are extended
|
5
|
-
# with these methods by default.
|
6
|
-
#
|
7
|
-
# @example to use in non-{Partitioned::PartitionedBase} class:
|
8
|
-
# class Company < ActiveRecord::Base
|
9
|
-
# extend Partitioned::BulkMethodsMixin
|
10
|
-
# end
|
11
|
-
#
|
12
|
-
module BulkMethodsMixin
|
13
|
-
# exception thrown when row data structures are inconsistent between rows in single call to {#create_many} or {#update_many}
|
14
|
-
class BulkUploadDataInconsistent < StandardError
|
15
|
-
def initialize(model, table_name, expected_columns, found_columns, while_doing)
|
16
|
-
super("#{model.name}: for table: #{table_name}; #{expected_columns} != #{found_columns}; #{while_doing}")
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
# BULK creation of many rows
|
21
|
-
#
|
22
|
-
# @example no options used
|
23
|
-
# rows = [
|
24
|
-
# { :name => 'Keith', :salary => 1000 },
|
25
|
-
# { :name => 'Alex', :salary => 2000 }
|
26
|
-
# ]
|
27
|
-
# Employee.create_many(rows)
|
28
|
-
#
|
29
|
-
# @example with :returning option to returns key value
|
30
|
-
# rows = [
|
31
|
-
# { :name => 'Keith', :salary => 1000 },
|
32
|
-
# { :name => 'Alex', :salary => 2000 }
|
33
|
-
# ]
|
34
|
-
# options = { :returning => [:id] }
|
35
|
-
# Employee.create_many(rows, options)
|
36
|
-
# [#<Employee id: 1>, #<Employee id: 2>]
|
37
|
-
#
|
38
|
-
# @example with :slice_size option (will generate two insert queries)
|
39
|
-
# rows = [
|
40
|
-
# { :name => 'Keith', :salary => 1000 },
|
41
|
-
# { :name => 'Alex', :salary => 2000 },
|
42
|
-
# { :name => 'Mark', :salary => 3000 }
|
43
|
-
# ]
|
44
|
-
# options = { :slice_size => 2 }
|
45
|
-
# Employee.create_many(rows, options)
|
46
|
-
#
|
47
|
-
# @param [Array<Hash>] rows ([]) data to be inserted into database
|
48
|
-
# @param [Hash] options ({}) options for bulk inserts
|
49
|
-
# @option options [Integer] :slice_size (1000) how many records will be created in a single SQL query
|
50
|
-
# @option options [Boolean] :check_consitency (true) ensure some modicum of sanity on the incoming dataset, specifically: does each row define the same set of key/value pairs
|
51
|
-
# @option options [Array or String] :returning (nil) list of fields to return.
|
52
|
-
# @return [Array<Hash>] rows returned from DB as option[:returning] requests
|
53
|
-
# @raise [BulkUploadDataInconsistent] raised when key/value pairs between rows are inconsistent (check disabled with option :check_consistency)
|
54
|
-
def create_many(rows, options = {})
|
55
|
-
return [] if rows.blank?
|
56
|
-
options[:slice_size] = 1000 unless options.has_key?(:slice_size)
|
57
|
-
options[:check_consistency] = true unless options.has_key?(:check_consistency)
|
58
|
-
returning_clause = ""
|
59
|
-
if options[:returning]
|
60
|
-
if options[:returning].is_a? Array
|
61
|
-
returning_list = options[:returning].join(',')
|
62
|
-
else
|
63
|
-
returning_list = options[:returning]
|
64
|
-
end
|
65
|
-
returning_clause = " returning #{returning_list}"
|
66
|
-
end
|
67
|
-
returning = []
|
68
|
-
|
69
|
-
created_at_value = Time.zone.now
|
70
|
-
|
71
|
-
num_sequences_needed = rows.reject{|r| r[:id].present?}.length
|
72
|
-
if num_sequences_needed > 0
|
73
|
-
row_ids = connection.next_sequence_values(sequence_name, num_sequences_needed)
|
74
|
-
else
|
75
|
-
row_ids = []
|
76
|
-
end
|
77
|
-
rows.each do |row|
|
78
|
-
# set the primary key if it needs to be set
|
79
|
-
row[:id] ||= row_ids.shift
|
80
|
-
end.each do |row|
|
81
|
-
# set :created_at if need be
|
82
|
-
row[:created_at] ||= created_at_value
|
83
|
-
end.group_by do |row|
|
84
|
-
respond_to?(:partition_table_name) ? partition_table_name(*partition_key_values(row)) : table_name
|
85
|
-
end.each do |table_name, rows_for_table|
|
86
|
-
column_names = rows_for_table[0].keys.sort{|a,b| a.to_s <=> b.to_s}
|
87
|
-
sql_insert_string = "insert into #{table_name} (#{column_names.join(',')}) values "
|
88
|
-
rows_for_table.map do |row|
|
89
|
-
if options[:check_consistency]
|
90
|
-
row_column_names = row.keys.sort{|a,b| a.to_s <=> b.to_s}
|
91
|
-
if column_names != row_column_names
|
92
|
-
raise BulkUploadDataInconsistent.new(self, table_name, column_names, row_column_names, "while attempting to build insert statement")
|
93
|
-
end
|
94
|
-
end
|
95
|
-
column_values = column_names.map do |column_name|
|
96
|
-
quote_value(row[column_name], columns_hash[column_name.to_s])
|
97
|
-
end.join(',')
|
98
|
-
"(#{column_values})"
|
99
|
-
end.each_slice(options[:slice_size]) do |insert_slice|
|
100
|
-
returning += find_by_sql(sql_insert_string + insert_slice.join(',') + returning_clause)
|
101
|
-
end
|
102
|
-
end
|
103
|
-
return returning
|
104
|
-
end
|
105
|
-
|
106
|
-
#
|
107
|
-
# BULK updates of many rows
|
108
|
-
#
|
109
|
-
# @return [Array<Hash>] rows returned from DB as option[:returning] requests
|
110
|
-
# @raise [BulkUploadDataInconsistent] raised when key/value pairs between rows are inconsistent (check disabled with option :check_consistency)
|
111
|
-
# @param [Hash] options ({}) options for bulk inserts
|
112
|
-
# @option options [Integer] :slice_size (1000) how many records will be created in a single SQL query
|
113
|
-
# @option options [Boolean] :check_consitency (true) ensure some modicum of sanity on the incoming dataset, specifically: does each row define the same set of key/value pairs
|
114
|
-
# @option options [Array] :returning (nil) list of fields to return.
|
115
|
-
# @option options [String] :returning (nil) single field to return.
|
116
|
-
#
|
117
|
-
# @overload update_many(rows = [], options = {})
|
118
|
-
# @param [Array<Hash>] rows ([]) data to be updated
|
119
|
-
# @option options [String] :set_array (built from first row passed in) the set clause
|
120
|
-
# @option options [String] :where ('"#{table_name}.id = datatable.id"') the where clause
|
121
|
-
#
|
122
|
-
# @overload update_many(rows = {}, options = {})
|
123
|
-
# @param [Hash<Hash, Hash>] rows ({}) data to be updated
|
124
|
-
# @option options [String] :set_array (built from the values in the first key/value pair of `rows`) the set clause
|
125
|
-
# @option options [String] :where (built from the keys in the first key/value pair of `rows`) the where clause
|
126
|
-
#
|
127
|
-
# @example using "set_array" to add the value of "salary" to the specific employee's salary the default where clause matches IDs so, it works here.
|
128
|
-
# rows = [
|
129
|
-
# { :id => 1, :salary => 1000 },
|
130
|
-
# { :id => 10, :salary => 2000 },
|
131
|
-
# { :id => 23, :salary => 2500 }
|
132
|
-
# ]
|
133
|
-
# options = { :set_array => '"salary = datatable.salary"' }
|
134
|
-
# Employee.update_many(rows, options)
|
135
|
-
#
|
136
|
-
# @example using where clause to match salary.
|
137
|
-
# rows = [
|
138
|
-
# { :id => 1, :salary => 1000, :company_id => 10 },
|
139
|
-
# { :id => 10, :salary => 2000, :company_id => 12 },
|
140
|
-
# { :id => 23, :salary => 2500, :company_id => 5 }
|
141
|
-
# ]
|
142
|
-
# options = {
|
143
|
-
# :set_array => '"company_id = datatable.company_id"',
|
144
|
-
# :where => '"#{table_name}.salary = datatable.salary"'
|
145
|
-
# }
|
146
|
-
# Employee.update_many(rows, options)
|
147
|
-
#
|
148
|
-
# @example setting where clause to the KEY of the hash passed in and the set_array is generated from the VALUES
|
149
|
-
# rows = {
|
150
|
-
# { :id => 1 } => { :salary => 100000, :company_id => 10 },
|
151
|
-
# { :id => 10 } => { :salary => 110000, :company_id => 12 },
|
152
|
-
# { :id => 23 } => { :salary => 90000, :company_id => 5 }
|
153
|
-
# }
|
154
|
-
# Employee.update_many(rows)
|
155
|
-
#
|
156
|
-
# @note Remember that you should probably set updated_at using "updated = datatable.updated_at"
|
157
|
-
# or "updated_at = now()" in the set_array if you want to follow
|
158
|
-
# the standard active record model for time columns (and you have an updated_at column)
|
159
|
-
def update_many(rows, options = {})
|
160
|
-
return [] if rows.blank?
|
161
|
-
if rows.is_a?(Hash)
|
162
|
-
options[:where] = '"' + rows.keys[0].keys.map{|key| '#{table_name}.' + "#{key} = datatable.#{key}"}.join(' and ') + '"'
|
163
|
-
options[:set_array] = '"' + rows.values[0].keys.map{|key| "#{key} = datatable.#{key}"}.join(',') + '"' unless options[:set_array]
|
164
|
-
r = []
|
165
|
-
rows.each do |key,value|
|
166
|
-
r << key.merge(value)
|
167
|
-
end
|
168
|
-
rows = r
|
169
|
-
end
|
170
|
-
unless options[:set_array]
|
171
|
-
column_names = rows[0].keys
|
172
|
-
columns_to_remove = [:id]
|
173
|
-
columns_to_remove += [partition_keys].map{|k| k.to_sym} if respond_to?(:partition_keys)
|
174
|
-
options[:set_array] = '"' + (column_names - columns_to_remove.flatten).map{|cn| "#{cn} = datatable.#{cn}"}.join(',') + '"'
|
175
|
-
end
|
176
|
-
options[:slice_size] = 1000 unless options[:slice_size]
|
177
|
-
options[:check_consistency] = true unless options.has_key?(:check_consistency)
|
178
|
-
returning_clause = ""
|
179
|
-
if options[:returning]
|
180
|
-
if options[:returning].is_a?(Array)
|
181
|
-
returning_list = options[:returning].map{|r| '#{table_name}.' + r.to_s}.join(',')
|
182
|
-
else
|
183
|
-
returning_list = options[:returning]
|
184
|
-
end
|
185
|
-
returning_clause = "\" returning #{returning_list}\""
|
186
|
-
end
|
187
|
-
options[:where] = '"#{table_name}.id = datatable.id"' unless options[:where]
|
188
|
-
|
189
|
-
returning = []
|
190
|
-
|
191
|
-
rows.group_by do |row|
|
192
|
-
respond_to?(:partition_table_name) ? partition_table_name(*partition_key_values(row)) : table_name
|
193
|
-
end.each do |table_name, rows_for_table|
|
194
|
-
column_names = rows_for_table[0].keys.sort{|a,b| a.to_s <=> b.to_s}
|
195
|
-
rows_for_table.each_slice(options[:slice_size]) do |update_slice|
|
196
|
-
datatable_rows = []
|
197
|
-
update_slice.each_with_index do |row,i|
|
198
|
-
if options[:check_consistency]
|
199
|
-
row_column_names = row.keys.sort{|a,b| a.to_s <=> b.to_s}
|
200
|
-
if column_names != row_column_names
|
201
|
-
raise BulkUploadDataInconsistent.new(self, table_name, column_names, row_column_names, "while attempting to build update statement")
|
202
|
-
end
|
203
|
-
end
|
204
|
-
datatable_rows << row.map do |column_name,column_value|
|
205
|
-
column_name = column_name.to_s
|
206
|
-
columns_hash_value = columns_hash[column_name]
|
207
|
-
if i == 0
|
208
|
-
"#{quote_value(column_value, columns_hash_value)}::#{columns_hash_value.sql_type} as #{column_name}"
|
209
|
-
else
|
210
|
-
quote_value(column_value, columns_hash_value)
|
211
|
-
end
|
212
|
-
end.join(',')
|
213
|
-
end
|
214
|
-
datatable = datatable_rows.join(' union select ')
|
215
|
-
|
216
|
-
sql_update_string = <<-SQL
|
217
|
-
update #{table_name} set
|
218
|
-
#{eval(options[:set_array])}
|
219
|
-
from
|
220
|
-
(select
|
221
|
-
#{datatable}
|
222
|
-
) as datatable
|
223
|
-
where
|
224
|
-
#{eval(options[:where])}
|
225
|
-
#{eval(returning_clause)}
|
226
|
-
SQL
|
227
|
-
returning += find_by_sql(sql_update_string)
|
228
|
-
end
|
229
|
-
end
|
230
|
-
return returning
|
231
|
-
end
|
232
|
-
end
|
233
|
-
end
|
@@ -1,512 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
require "#{File.dirname(__FILE__)}/../support/tables_spec_helper"
|
3
|
-
|
4
|
-
module Partitioned
|
5
|
-
module BulkMethodsMixin
|
6
|
-
describe "BulkMethodsMixin" do
|
7
|
-
include TablesSpecHelper
|
8
|
-
|
9
|
-
before do
|
10
|
-
class Employee < ActiveRecord::Base
|
11
|
-
include ActiveRecordOverrides
|
12
|
-
extend Partitioned::BulkMethodsMixin
|
13
|
-
end
|
14
|
-
create_tables
|
15
|
-
end
|
16
|
-
|
17
|
-
describe "create_many" do
|
18
|
-
|
19
|
-
context "when call method with empty rows" do
|
20
|
-
it "returns empty array" do
|
21
|
-
Employee.create_many("").should == []
|
22
|
-
end
|
23
|
-
end # when call method with empty rows
|
24
|
-
|
25
|
-
context "when try to create records with the given id" do
|
26
|
-
it "records created" do
|
27
|
-
Employee.create_many([{ :id => Employee.connection.next_sequence_value(Employee.sequence_name),
|
28
|
-
:name => 'Keith',
|
29
|
-
:company_id => 2
|
30
|
-
},
|
31
|
-
{ :id => Employee.connection.next_sequence_value(Employee.sequence_name),
|
32
|
-
:name => 'Mike',
|
33
|
-
:company_id => 3
|
34
|
-
},
|
35
|
-
{ :id => Employee.connection.next_sequence_value(Employee.sequence_name),
|
36
|
-
:name => 'Alex',
|
37
|
-
:company_id => 1
|
38
|
-
}])
|
39
|
-
Employee.all.map{ |r| r.name }.should == ["Keith", "Mike", "Alex"]
|
40
|
-
end
|
41
|
-
end # when try to create records with the given id
|
42
|
-
|
43
|
-
context "when try to create records without the given id" do
|
44
|
-
it "records created" do
|
45
|
-
Employee.create_many([{ :name => 'Keith', :company_id => 2 },
|
46
|
-
{ :name => 'Mike', :company_id => 3 },
|
47
|
-
{ :name => 'Alex', :company_id => 1 }])
|
48
|
-
Employee.all.map{ |r| r.name }.should == ["Keith", "Mike", "Alex"]
|
49
|
-
end
|
50
|
-
end # when try to create records without the given id
|
51
|
-
|
52
|
-
context "when try to create records with a mixture of given ids and non-given ids" do
|
53
|
-
it "records created" do
|
54
|
-
Employee.create_many([{ :name => 'Keith', :company_id => 2 },
|
55
|
-
{ :id => Employee.connection.next_sequence_value(Employee.sequence_name),
|
56
|
-
:name => 'Mike',
|
57
|
-
:company_id => 3
|
58
|
-
},
|
59
|
-
{ :name => 'Mark', :company_id => 1 },
|
60
|
-
{ :id => Employee.connection.next_sequence_value(Employee.sequence_name),
|
61
|
-
:name => 'Alex',
|
62
|
-
:company_id => 1
|
63
|
-
}])
|
64
|
-
Employee.all.map{ |r| r.name }.should == ["Keith", "Mike", "Mark", "Alex"]
|
65
|
-
end
|
66
|
-
end # when try to create records with a mixture of given ids and non-given ids
|
67
|
-
|
68
|
-
context "when try to create records with the given created_at" do
|
69
|
-
it "records created" do
|
70
|
-
Employee.create_many([{ :name => 'Keith',
|
71
|
-
:company_id => 2,
|
72
|
-
:created_at => Time.zone.parse('2012-01-02')
|
73
|
-
},
|
74
|
-
{ :name => 'Mike',
|
75
|
-
:company_id => 3,
|
76
|
-
:created_at => Time.zone.parse('2012-01-03')
|
77
|
-
},
|
78
|
-
{ :name => 'Alex',
|
79
|
-
:company_id => 1,
|
80
|
-
:created_at => Time.zone.parse('2012-01-04')
|
81
|
-
}])
|
82
|
-
Employee.all.map{ |r| r.created_at }.should == [
|
83
|
-
Time.zone.parse('2012-01-02'),
|
84
|
-
Time.zone.parse('2012-01-03'),
|
85
|
-
Time.zone.parse('2012-01-04')
|
86
|
-
]
|
87
|
-
end
|
88
|
-
end # when try to create records with the given created_at
|
89
|
-
|
90
|
-
context "when try to create records without the given created_at" do
|
91
|
-
it "records created" do
|
92
|
-
Employee.create_many([{ :name => 'Keith', :company_id => 2 },
|
93
|
-
{ :name => 'Mike', :company_id => 3 },
|
94
|
-
{ :name => 'Alex', :company_id => 1 }])
|
95
|
-
Employee.all.each{ |r| r.created_at.between?(Time.now - 3.minute, Time.now + 3.minute) }.
|
96
|
-
should be_true
|
97
|
-
end
|
98
|
-
end # when try to create records without the given created_at
|
99
|
-
|
100
|
-
context "when try to create records without options" do
|
101
|
-
it "generates one insert queries" do
|
102
|
-
Employee.should_receive(:find_by_sql).once.and_return([])
|
103
|
-
Employee.create_many([{ :name => 'Keith', :company_id => 2 },
|
104
|
-
{ :name => 'Alex', :company_id => 1 },
|
105
|
-
{ :name => 'Mark', :company_id => 2 },
|
106
|
-
{ :name => 'Phil', :company_id => 3 }])
|
107
|
-
end
|
108
|
-
end # when try to create records without options
|
109
|
-
|
110
|
-
context "when call method with option 'slice_size' equal 2" do
|
111
|
-
it "generates two insert queries" do
|
112
|
-
Employee.should_receive(:find_by_sql).twice.and_return([])
|
113
|
-
Employee.create_many([{ :name => 'Keith', :company_id => 2 },
|
114
|
-
{ :name => 'Alex', :company_id => 1 },
|
115
|
-
{ :name => 'Mark', :company_id => 2 },
|
116
|
-
{ :name => 'Phil', :company_id => 3 }],
|
117
|
-
{ :slice_size => 2})
|
118
|
-
end
|
119
|
-
end # when call method with option 'slice_size' equal 2
|
120
|
-
|
121
|
-
context "when create two records with options 'returning' equal id" do
|
122
|
-
it "returns last records id" do
|
123
|
-
Employee.create_many([{ :name => 'Keith', :company_id => 2 },
|
124
|
-
{ :name => 'Alex', :company_id => 3 }],
|
125
|
-
{ :returning => [:id] }).
|
126
|
-
last.id.should == 2
|
127
|
-
end
|
128
|
-
end # when create two records with options 'returning' equal id
|
129
|
-
|
130
|
-
context "when try to create two records and doesn't
|
131
|
-
the same number of keys and options check_consistency equal false" do
|
132
|
-
it "records created, last salary is nil" do
|
133
|
-
Employee.create_many([{ :company_id => 2, :name => 'Keith', :salary => 1002 },
|
134
|
-
{ :name => 'Alex', :company_id => 3 }],
|
135
|
-
{ :check_consistency => false })
|
136
|
-
Employee.find(2).salary.should == nil
|
137
|
-
end
|
138
|
-
end # when try to create two records and doesn't
|
139
|
-
# the same number of keys and options check_consistency equal false
|
140
|
-
|
141
|
-
context "when try to create two records and doesn't the same number of keys" do
|
142
|
-
it "raises BulkUploadDataInconsistent" do
|
143
|
-
lambda { Employee.create_many([{ :company_id => 2, :name => 'Keith', :salary => 1002 },
|
144
|
-
{ :name => 'Alex', :company_id => 3}])
|
145
|
-
}.should raise_error(BulkUploadDataInconsistent)
|
146
|
-
end
|
147
|
-
end # when try to create two records and doesn't the same number of keys
|
148
|
-
|
149
|
-
context "when try to create records using partitioning" do
|
150
|
-
|
151
|
-
before do
|
152
|
-
Partitioned::BulkMethodsMixin.send(:remove_const, :Employee)
|
153
|
-
class Employee < ByForeignKey
|
154
|
-
belongs_to :company, :class_name => 'Company'
|
155
|
-
|
156
|
-
def self.partition_foreign_key
|
157
|
-
return :company_id
|
158
|
-
end
|
159
|
-
|
160
|
-
partitioned do |partition|
|
161
|
-
partition.index :id, :unique => true
|
162
|
-
partition.foreign_key :company_id
|
163
|
-
end
|
164
|
-
end # Employee
|
165
|
-
Employee.create_new_partition_tables(Employee.partition_generate_range(0, 4, 1))
|
166
|
-
end
|
167
|
-
|
168
|
-
after do
|
169
|
-
Partitioned::BulkMethodsMixin.send(:remove_const, :Employee)
|
170
|
-
end
|
171
|
-
|
172
|
-
it "returns records" do
|
173
|
-
Employee.create_many([{ :name => 'Keith', :company_id => 2 },
|
174
|
-
{ :name => 'Alex', :company_id => 1 },
|
175
|
-
{ :name => 'Phil', :company_id => 3 }])
|
176
|
-
Employee.from_partition(1).where(:id => 2).first.name.should == "Alex"
|
177
|
-
Employee.where(:id => 1, :company_id => 2).first.name.should == "Keith"
|
178
|
-
Employee.all.map{ |r| r.name }.should == ["Alex", "Keith", "Phil"]
|
179
|
-
end
|
180
|
-
end # when try to create records using partitioning
|
181
|
-
|
182
|
-
context "when try to create records in the table that has all the different sql types" do
|
183
|
-
|
184
|
-
before do
|
185
|
-
ActiveRecord::Base.connection.execute <<-SQL
|
186
|
-
ALTER TABLE employees ADD COLUMN test_string character varying;
|
187
|
-
ALTER TABLE employees ADD COLUMN test_float float;
|
188
|
-
ALTER TABLE employees ADD COLUMN test_decimal decimal;
|
189
|
-
ALTER TABLE employees ADD COLUMN test_time time;
|
190
|
-
ALTER TABLE employees ADD COLUMN test_time_string time;
|
191
|
-
ALTER TABLE employees ADD COLUMN test_date date;
|
192
|
-
ALTER TABLE employees ADD COLUMN test_date_string date;
|
193
|
-
ALTER TABLE employees ADD COLUMN test_bytea bytea;
|
194
|
-
ALTER TABLE employees ADD COLUMN test_boolean boolean;
|
195
|
-
ALTER TABLE employees ADD COLUMN test_xml xml;
|
196
|
-
ALTER TABLE employees ADD COLUMN test_tsvector tsvector;
|
197
|
-
SQL
|
198
|
-
Employee.reset_column_information
|
199
|
-
end
|
200
|
-
|
201
|
-
after do
|
202
|
-
ActiveRecord::Base.connection.reset!
|
203
|
-
end
|
204
|
-
|
205
|
-
context "non-null values" do
|
206
|
-
it "returns record with all sql types" do
|
207
|
-
lambda { Employee.create_many([{ :name => 'Keith',
|
208
|
-
:company_id => 2,
|
209
|
-
:created_at => Time.zone.parse('2012-12-21'),
|
210
|
-
:updated_at => '2012-12-21 00:00:00',
|
211
|
-
:test_string => "string",
|
212
|
-
:test_float => 12.34,
|
213
|
-
:test_decimal => 123456789101112,
|
214
|
-
:test_time => Time.now,
|
215
|
-
:test_time_string => '00:00:00',
|
216
|
-
:test_date => Date.parse('2012-12-21'),
|
217
|
-
:test_date_string => '2012-12-21',
|
218
|
-
:test_bytea => "text".bytes.to_a,
|
219
|
-
:test_boolean => false,
|
220
|
-
:test_xml => ["text"].to_xml,
|
221
|
-
:test_tsvector => "test string",
|
222
|
-
}]) }.should_not raise_error
|
223
|
-
Employee.all.size.should == 1
|
224
|
-
end
|
225
|
-
end # non-null values
|
226
|
-
|
227
|
-
context "null values" do
|
228
|
-
it "returns record with all sql types" do
|
229
|
-
lambda { Employee.create_many([{ :name => 'Keith',
|
230
|
-
:company_id => 2,
|
231
|
-
:created_at => nil,
|
232
|
-
:updated_at => nil,
|
233
|
-
:salary => nil,
|
234
|
-
:test_string => nil,
|
235
|
-
:test_float => nil,
|
236
|
-
:test_decimal => nil,
|
237
|
-
:test_time => nil,
|
238
|
-
:test_time_string => nil,
|
239
|
-
:test_date => nil,
|
240
|
-
:test_date_string => nil,
|
241
|
-
:test_bytea => nil,
|
242
|
-
:test_boolean => nil,
|
243
|
-
:test_xml => nil,
|
244
|
-
:test_tsvector => nil,
|
245
|
-
}]) }.should_not raise_error
|
246
|
-
Employee.all.size.should == 1
|
247
|
-
end
|
248
|
-
end # null values
|
249
|
-
|
250
|
-
end # when try to create records in the table that has all the different sql types
|
251
|
-
|
252
|
-
end # create_many
|
253
|
-
|
254
|
-
describe "update_many" do
|
255
|
-
|
256
|
-
before do
|
257
|
-
Employee.create_many([{ :name => 'Keith', :company_id => 2 },
|
258
|
-
{ :name => 'Alex', :company_id => 1 },
|
259
|
-
{ :name => 'Mark', :company_id => 2 },
|
260
|
-
{ :name => 'Phil', :company_id => 3 }])
|
261
|
-
end
|
262
|
-
|
263
|
-
context "when call method with empty rows" do
|
264
|
-
it "returns empty array" do
|
265
|
-
Employee.update_many("").should == []
|
266
|
-
end
|
267
|
-
end # when call method with empty rows
|
268
|
-
|
269
|
-
context "when try to update records without options" do
|
270
|
-
|
271
|
-
context "input parameters is hash" do
|
272
|
-
it "records updated" do
|
273
|
-
Employee.update_many({ { :id => 1 } => {
|
274
|
-
:name => 'Elvis'
|
275
|
-
},
|
276
|
-
{ :id => 2 } => {
|
277
|
-
:name => 'Freddi'
|
278
|
-
} })
|
279
|
-
Employee.find(1).name.should == "Elvis"
|
280
|
-
Employee.find(2).name.should == "Freddi"
|
281
|
-
end
|
282
|
-
end # input parameters is hash
|
283
|
-
|
284
|
-
context "input parameters is array" do
|
285
|
-
it "records updated" do
|
286
|
-
Employee.update_many([{ :id => 1,
|
287
|
-
:name => 'Elvis'
|
288
|
-
},
|
289
|
-
{ :id => 2,
|
290
|
-
:name => 'Freddi'
|
291
|
-
}])
|
292
|
-
Employee.find(1).name.should == "Elvis"
|
293
|
-
Employee.find(2).name.should == "Freddi"
|
294
|
-
end
|
295
|
-
end # input parameters is array
|
296
|
-
|
297
|
-
context "when try to update two records and doesn't the same number of keys" do
|
298
|
-
it "raises BulkUploadDataInconsistent" do
|
299
|
-
lambda { Employee.update_many([{ :id => 1, :name => 'Elvis', :salary => 1002 },
|
300
|
-
{ :name => 'Freddi', :id => 2}])
|
301
|
-
}.should raise_error(BulkUploadDataInconsistent)
|
302
|
-
end
|
303
|
-
end # when try to update two records and doesn't the same number of keys
|
304
|
-
|
305
|
-
context "when try to update records with the given updated_at" do
|
306
|
-
it "records created" do
|
307
|
-
Employee.update_many([{ :id => 1,
|
308
|
-
:updated_at => Time.zone.parse('2012-01-02')
|
309
|
-
},
|
310
|
-
{ :id => 2,
|
311
|
-
:updated_at => Time.zone.parse('2012-01-03')
|
312
|
-
},
|
313
|
-
{ :id => 3,
|
314
|
-
:updated_at => Time.zone.parse('2012-01-04')
|
315
|
-
},
|
316
|
-
{ :id => 4,
|
317
|
-
:updated_at => Time.zone.parse('2012-01-05')
|
318
|
-
}])
|
319
|
-
Employee.all.map{ |r| r.updated_at }.should == [
|
320
|
-
Time.zone.parse('2012-01-02'),
|
321
|
-
Time.zone.parse('2012-01-03'),
|
322
|
-
Time.zone.parse('2012-01-04'),
|
323
|
-
Time.zone.parse('2012-01-05')
|
324
|
-
]
|
325
|
-
end
|
326
|
-
end # when try to update records with the given updated_at
|
327
|
-
|
328
|
-
end # when try to update records without options
|
329
|
-
|
330
|
-
context "when call method with option :slice_size set is default" do
|
331
|
-
it "generates one insert queries" do
|
332
|
-
Employee.should_receive(:find_by_sql).once.and_return([])
|
333
|
-
Employee.update_many([{ :id => 1, :name => 'Elvis' },
|
334
|
-
{ :id => 2, :name => 'Freddi'},
|
335
|
-
{ :id => 3, :name => 'Patric'},
|
336
|
-
{ :id => 4, :name => 'Jane'}])
|
337
|
-
end
|
338
|
-
end # when call method with option :slice_size set is default
|
339
|
-
|
340
|
-
|
341
|
-
context "when call method with option :slice_size = 2" do
|
342
|
-
it "generates two insert queries" do
|
343
|
-
Employee.should_receive(:find_by_sql).twice.and_return([])
|
344
|
-
Employee.update_many([{ :id => 1, :name => 'Elvis' },
|
345
|
-
{ :id => 2, :name => 'Freddi'},
|
346
|
-
{ :id => 3, :name => 'Patric'},
|
347
|
-
{ :id => 4, :name => 'Jane'}],
|
348
|
-
{ :slice_size => 2})
|
349
|
-
end
|
350
|
-
end # when call method with option :slice_size = 2
|
351
|
-
|
352
|
-
context "when try to update two records and doesn't
|
353
|
-
the same number of keys and options check_consistency equal false" do
|
354
|
-
it "raises ActiveRecord::StatementInvalid" do
|
355
|
-
lambda {
|
356
|
-
Employee.update_many([{ :id => 1, :name => 'Elvis', :salary => 1002 },
|
357
|
-
{ :name => 'Freddi', :id => 2}],
|
358
|
-
{ :check_consistency => false })
|
359
|
-
}.should raise_error(ActiveRecord::StatementInvalid)
|
360
|
-
end
|
361
|
-
end # when try to update two records and doesn't
|
362
|
-
# the same number of keys and options check_consistency equal false
|
363
|
-
|
364
|
-
context "when update two records with options 'returning' equal :name" do
|
365
|
-
it "returns last records name" do
|
366
|
-
Employee.update_many([{ :id => 1, :name => 'Elvis' },
|
367
|
-
{ :id => 2, :name => 'Freddi'}],
|
368
|
-
{ :returning => [:name] }).
|
369
|
-
last.name.should == 'Freddi'
|
370
|
-
end
|
371
|
-
end # when update two records with options 'returning' equal :name
|
372
|
-
|
373
|
-
context "when update method with options :set_array equal 'salary = datatable.salary'" do
|
374
|
-
it "updates only salary column" do
|
375
|
-
Employee.update_many([{ :id => 1, :name => 'Elvis', :salary => 12 },
|
376
|
-
{ :id => 2, :name => 'Freddi',:salary => 22}],
|
377
|
-
{ :set_array => '"salary = datatable.salary"' })
|
378
|
-
Employee.find(1).name.should_not == "Elvis"
|
379
|
-
Employee.find(1).salary.should == 12
|
380
|
-
Employee.find(2).name.should_not == "Freddi"
|
381
|
-
Employee.find(2).salary.should == 22
|
382
|
-
end
|
383
|
-
end # when update method with options :set_array equal 'salary = datatable.salary'
|
384
|
-
|
385
|
-
context "when update method with options :where" do
|
386
|
-
it "updates only name column, where salary equal input values" do
|
387
|
-
Employee.update_many([{ :id => 1, :name => 'Elvis', :salary => 12 },
|
388
|
-
{ :id => 2, :name => 'Freddi',:salary => 22}],
|
389
|
-
{ :where => '"#{table_name}.salary = datatable.salary"' })
|
390
|
-
Employee.find(1).name.should_not == "Elvis"
|
391
|
-
Employee.find(1).salary.should == 3
|
392
|
-
Employee.find(2).name.should_not == "Freddi"
|
393
|
-
Employee.find(2).salary.should == 3
|
394
|
-
end
|
395
|
-
end # when update method with options :where
|
396
|
-
|
397
|
-
context "when try to update records using partitioning" do
|
398
|
-
|
399
|
-
before do
|
400
|
-
drop_tables
|
401
|
-
create_tables
|
402
|
-
Partitioned::BulkMethodsMixin.send(:remove_const, :Employee)
|
403
|
-
class Employee < ByForeignKey
|
404
|
-
belongs_to :company, :class_name => 'Company'
|
405
|
-
|
406
|
-
def self.partition_foreign_key
|
407
|
-
return :company_id
|
408
|
-
end
|
409
|
-
|
410
|
-
partitioned do |partition|
|
411
|
-
partition.index :id, :unique => true
|
412
|
-
partition.foreign_key :company_id
|
413
|
-
end
|
414
|
-
end # Employee
|
415
|
-
Employee.create_new_partition_tables(Employee.partition_generate_range(0, 4, 1))
|
416
|
-
Employee.create_many([{ :name => 'Keith', :company_id => 2 },
|
417
|
-
{ :name => 'Alex', :company_id => 1 },
|
418
|
-
{ :name => 'Mark', :company_id => 3 }])
|
419
|
-
end
|
420
|
-
|
421
|
-
after do
|
422
|
-
Partitioned::BulkMethodsMixin.send(:remove_const, :Employee)
|
423
|
-
end
|
424
|
-
|
425
|
-
it "returns records" do
|
426
|
-
Employee.update_many({ { :company_id => 2, :id => 1 } => { :name => 'Indy' },
|
427
|
-
{ :company_id => 1, :id => 2 } => { :name => 'Larry' },
|
428
|
-
{ :company_id => 3, :id => 3 } => { :name => 'Filip' } })
|
429
|
-
Employee.from_partition(1).where(:id => 2).first.name.should == "Larry"
|
430
|
-
Employee.where(:id => 1, :company_id => 2).first.name.should == "Indy"
|
431
|
-
Employee.all.map{ |r| r.name }.should == ["Larry", "Indy", "Filip"]
|
432
|
-
end
|
433
|
-
end # when try to update records using partitioning
|
434
|
-
|
435
|
-
context "when try to update records in the table that has all the different sql types" do
|
436
|
-
|
437
|
-
before do
|
438
|
-
ActiveRecord::Base.connection.execute <<-SQL
|
439
|
-
ALTER TABLE employees ADD COLUMN test_string character varying;
|
440
|
-
ALTER TABLE employees ADD COLUMN test_float float;
|
441
|
-
ALTER TABLE employees ADD COLUMN test_decimal decimal;
|
442
|
-
ALTER TABLE employees ADD COLUMN test_time time;
|
443
|
-
ALTER TABLE employees ADD COLUMN test_time_string time;
|
444
|
-
ALTER TABLE employees ADD COLUMN test_date date;
|
445
|
-
ALTER TABLE employees ADD COLUMN test_date_string date;
|
446
|
-
ALTER TABLE employees ADD COLUMN test_bytea bytea;
|
447
|
-
ALTER TABLE employees ADD COLUMN test_boolean boolean;
|
448
|
-
ALTER TABLE employees ADD COLUMN test_xml xml;
|
449
|
-
ALTER TABLE employees ADD COLUMN test_tsvector tsvector;
|
450
|
-
SQL
|
451
|
-
Employee.reset_column_information
|
452
|
-
end
|
453
|
-
|
454
|
-
after do
|
455
|
-
ActiveRecord::Base.connection.reset!
|
456
|
-
end
|
457
|
-
|
458
|
-
context "non-null values" do
|
459
|
-
it "returns record with all sql types" do
|
460
|
-
lambda { Employee.update_many([{ :id => 1,
|
461
|
-
:name => 'Keith',
|
462
|
-
:company_id => 2,
|
463
|
-
:created_at => Time.zone.parse('2012-12-21'),
|
464
|
-
:updated_at => '2012-12-21 00:00:00',
|
465
|
-
:test_string => "string",
|
466
|
-
:test_float => 12.34,
|
467
|
-
:test_decimal => 123456789101112,
|
468
|
-
:test_time => Time.now,
|
469
|
-
:test_time_string => '00:00:00',
|
470
|
-
:test_date => Date.parse('2012-12-21'),
|
471
|
-
:test_date_string => '2012-12-21',
|
472
|
-
:test_bytea => "text".bytes.to_a,
|
473
|
-
:test_boolean => false,
|
474
|
-
:test_xml => ["text"].to_xml,
|
475
|
-
:test_tsvector => "test string",
|
476
|
-
}]) }.should_not raise_error
|
477
|
-
Employee.find(1).test_boolean.should == false
|
478
|
-
Employee.find(1).test_tsvector.should == "'string' 'test'"
|
479
|
-
end
|
480
|
-
end # non-null values
|
481
|
-
|
482
|
-
context "null values" do
|
483
|
-
it "returns record with all sql types" do
|
484
|
-
lambda { Employee.update_many([{ :id => 1,
|
485
|
-
:name => 'Keith',
|
486
|
-
:company_id => 2,
|
487
|
-
:updated_at => nil,
|
488
|
-
:salary => nil,
|
489
|
-
:test_string => nil,
|
490
|
-
:test_float => nil,
|
491
|
-
:test_decimal => nil,
|
492
|
-
:test_time => nil,
|
493
|
-
:test_time_string => nil,
|
494
|
-
:test_date => nil,
|
495
|
-
:test_date_string => nil,
|
496
|
-
:test_bytea => nil,
|
497
|
-
:test_boolean => nil,
|
498
|
-
:test_xml => nil,
|
499
|
-
:test_tsvector => nil,
|
500
|
-
}]) }.should_not raise_error
|
501
|
-
Employee.find(1).test_boolean.should == nil
|
502
|
-
Employee.find(1).test_tsvector.should == nil
|
503
|
-
end
|
504
|
-
end # null values
|
505
|
-
|
506
|
-
end # when try to update records in the table that has all the different sql types
|
507
|
-
|
508
|
-
end # update_many
|
509
|
-
|
510
|
-
end # BulkMethodsMixin
|
511
|
-
end # BulkMethodsMixin
|
512
|
-
end # Partitioned
|