partitioned 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (95) hide show
  1. data/Gemfile +17 -0
  2. data/LICENSE +30 -0
  3. data/PARTITIONING_EXPLAINED.txt +351 -0
  4. data/README +111 -0
  5. data/Rakefile +27 -0
  6. data/examples/README +23 -0
  7. data/examples/company_id.rb +417 -0
  8. data/examples/company_id_and_created_at.rb +689 -0
  9. data/examples/created_at.rb +590 -0
  10. data/examples/created_at_referencing_awards.rb +1000 -0
  11. data/examples/id.rb +475 -0
  12. data/examples/lib/by_company_id.rb +11 -0
  13. data/examples/lib/command_line_tool_mixin.rb +71 -0
  14. data/examples/lib/company.rb +29 -0
  15. data/examples/lib/get_options.rb +44 -0
  16. data/examples/lib/roman.rb +41 -0
  17. data/examples/start_date.rb +621 -0
  18. data/init.rb +1 -0
  19. data/lib/monkey_patch_activerecord.rb +92 -0
  20. data/lib/monkey_patch_postgres.rb +73 -0
  21. data/lib/partitioned.rb +26 -0
  22. data/lib/partitioned/active_record_overrides.rb +34 -0
  23. data/lib/partitioned/bulk_methods_mixin.rb +288 -0
  24. data/lib/partitioned/by_created_at.rb +13 -0
  25. data/lib/partitioned/by_foreign_key.rb +21 -0
  26. data/lib/partitioned/by_id.rb +35 -0
  27. data/lib/partitioned/by_integer_field.rb +32 -0
  28. data/lib/partitioned/by_monthly_time_field.rb +23 -0
  29. data/lib/partitioned/by_time_field.rb +65 -0
  30. data/lib/partitioned/by_weekly_time_field.rb +30 -0
  31. data/lib/partitioned/multi_level.rb +20 -0
  32. data/lib/partitioned/multi_level/configurator/data.rb +14 -0
  33. data/lib/partitioned/multi_level/configurator/dsl.rb +32 -0
  34. data/lib/partitioned/multi_level/configurator/reader.rb +162 -0
  35. data/lib/partitioned/multi_level/partition_manager.rb +47 -0
  36. data/lib/partitioned/partitioned_base.rb +354 -0
  37. data/lib/partitioned/partitioned_base/configurator.rb +6 -0
  38. data/lib/partitioned/partitioned_base/configurator/data.rb +62 -0
  39. data/lib/partitioned/partitioned_base/configurator/dsl.rb +628 -0
  40. data/lib/partitioned/partitioned_base/configurator/reader.rb +209 -0
  41. data/lib/partitioned/partitioned_base/partition_manager.rb +138 -0
  42. data/lib/partitioned/partitioned_base/sql_adapter.rb +286 -0
  43. data/lib/partitioned/version.rb +3 -0
  44. data/lib/tasks/desirable_tasks.rake +4 -0
  45. data/partitioned.gemspec +21 -0
  46. data/spec/dummy/.rspec +1 -0
  47. data/spec/dummy/README.rdoc +261 -0
  48. data/spec/dummy/Rakefile +7 -0
  49. data/spec/dummy/app/assets/javascripts/application.js +9 -0
  50. data/spec/dummy/app/assets/stylesheets/application.css +7 -0
  51. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  52. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  53. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  54. data/spec/dummy/config.ru +4 -0
  55. data/spec/dummy/config/application.rb +51 -0
  56. data/spec/dummy/config/boot.rb +10 -0
  57. data/spec/dummy/config/database.yml +32 -0
  58. data/spec/dummy/config/environment.rb +5 -0
  59. data/spec/dummy/config/environments/development.rb +30 -0
  60. data/spec/dummy/config/environments/production.rb +60 -0
  61. data/spec/dummy/config/environments/test.rb +39 -0
  62. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  63. data/spec/dummy/config/initializers/inflections.rb +10 -0
  64. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  65. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  66. data/spec/dummy/config/initializers/session_store.rb +8 -0
  67. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  68. data/spec/dummy/config/locales/en.yml +5 -0
  69. data/spec/dummy/config/routes.rb +58 -0
  70. data/spec/dummy/public/404.html +26 -0
  71. data/spec/dummy/public/422.html +26 -0
  72. data/spec/dummy/public/500.html +26 -0
  73. data/spec/dummy/public/favicon.ico +0 -0
  74. data/spec/dummy/script/rails +6 -0
  75. data/spec/dummy/spec/spec_helper.rb +27 -0
  76. data/spec/monkey_patch_posgres_spec.rb +176 -0
  77. data/spec/partitioned/bulk_methods_mixin_spec.rb +512 -0
  78. data/spec/partitioned/by_created_at_spec.rb +62 -0
  79. data/spec/partitioned/by_foreign_key_spec.rb +95 -0
  80. data/spec/partitioned/by_id_spec.rb +97 -0
  81. data/spec/partitioned/by_integer_field_spec.rb +143 -0
  82. data/spec/partitioned/by_monthly_time_field_spec.rb +100 -0
  83. data/spec/partitioned/by_time_field_spec.rb +182 -0
  84. data/spec/partitioned/by_weekly_time_field_spec.rb +100 -0
  85. data/spec/partitioned/multi_level/configurator/dsl_spec.rb +88 -0
  86. data/spec/partitioned/multi_level/configurator/reader_spec.rb +147 -0
  87. data/spec/partitioned/partitioned_base/configurator/dsl_spec.rb +459 -0
  88. data/spec/partitioned/partitioned_base/configurator/reader_spec.rb +513 -0
  89. data/spec/partitioned/partitioned_base/sql_adapter_spec.rb +204 -0
  90. data/spec/partitioned/partitioned_base_spec.rb +173 -0
  91. data/spec/spec_helper.rb +32 -0
  92. data/spec/support/shared_example_spec_helper_for_integer_key.rb +137 -0
  93. data/spec/support/shared_example_spec_helper_for_time_key.rb +147 -0
  94. data/spec/support/tables_spec_helper.rb +47 -0
  95. metadata +250 -0
@@ -0,0 +1,47 @@
1
+ module Partitioned
2
+ class MultiLevel
3
+ class PartitionManager < Partitioned::PartitionedBase::PartitionManager
4
+ #
5
+ # the once called function to prepare a parent table for partitioning as well
6
+ # as create the schema that the child tables will be placed in.
7
+ #
8
+ def create_infrastructure(enumerable = [[]])
9
+ super()
10
+ enumerable.each do |*partition_key_values|
11
+ create_partition_schema(*partition_key_values)
12
+ end
13
+ end
14
+
15
+ protected
16
+
17
+ #
18
+ # create a specific child table that does not currently
19
+ # exist and whose schema (the schema that the table exists in)
20
+ # also already exists (#create_infrastructure is designed to
21
+ # create this).
22
+ #
23
+ def create_new_partition(*partition_key_values)
24
+ create_partition_table(*partition_key_values)
25
+ if is_leaf_partition?(*partition_key_values)
26
+ add_partition_table_index(*partition_key_values)
27
+ add_references_to_partition_table(*partition_key_values)
28
+ else
29
+ add_parent_table_rules(*partition_key_values)
30
+ end
31
+ end
32
+
33
+ #
34
+ # is the table a child table without itself having any children.
35
+ # generally leaf tables are where all indexes and foreign key
36
+ # constraints will be placed because that is where the data will be.
37
+ #
38
+ # Non leaf tables will typically have a rule placed on them
39
+ # (via add_parent_table_rules) that prevents any inserts from occuring
40
+ # on them.
41
+ #
42
+ def is_leaf_partition?(*partition_key_values)
43
+ return partition_key_values.length == parent_table_class.configurator.on_fields.length
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,354 @@
1
+ #
2
+ # :include: ../../README
3
+ #
4
+ module Partitioned
5
+ #
6
+ # used by PartitionedBase class methods that must be overidden.
7
+ #
8
+ class MethodNotImplemented < StandardError
9
+ def initialize(model, method_name, is_class_method = true)
10
+ super("#{model.name}#{is_class_method ? '.' : '#'}#{method_name}")
11
+ end
12
+ end
13
+
14
+ #
15
+ # PartitionedBase
16
+ # an ActiveRecord::Base class that can be partitioned.
17
+ #
18
+ # Uses a domain specific language to configure, see Partitioned::PartitionedBase::Configurator
19
+ # for more information
20
+ #
21
+ # Extends Partitioned::BulkMethodsMixin to provide create_many and update_many
22
+ #
23
+ # Uses PartitionManager to manage creation of child tables
24
+ #
25
+ # Monkey patches some ActiveRecord routines to call back to this class when INSERT and UPDATE
26
+ # statements are built (to determine the table_name with respect to values being inserted or updated)
27
+ #
28
+ class PartitionedBase < ActiveRecord::Base
29
+ include ActiveRecordOverrides
30
+ extend Partitioned::BulkMethodsMixin
31
+
32
+ self.abstract_class = true
33
+
34
+ #
35
+ # returns an array of attribute names (strings) used to fetch the key value(s)
36
+ # the determine this specific partition table.
37
+ #
38
+ def self.partition_keys
39
+ return configurator.on_fields
40
+ end
41
+
42
+ #
43
+ # the specific values for a partition of this active record's type which are defined by
44
+ # #partition_keys
45
+ #
46
+ def self.partition_key_values(values)
47
+ symbolized_values = values.symbolize_keys
48
+ return self.partition_keys.map{|key| symbolized_values[key.to_sym]}
49
+ end
50
+
51
+ #
52
+ # the name of the current partition table determined by this active records attributes that
53
+ # define the key value(s) for the constraint check.
54
+ #
55
+ def partition_table_name
56
+ symbolized_attributes = attributes.symbolize_keys
57
+ return self.class.partition_name(*self.class.partition_keys.map{|attribute_name| symbolized_attributes[attribute_name]})
58
+ end
59
+
60
+ #
61
+ # normalize the value to be used for partitioning. This allows, for instance, a class that partitions on
62
+ # a time field to group the times by month. An integer field might be grouped by every 10mil values, A
63
+ # string field might be grouped by its first character.
64
+ #
65
+ def self.partition_normalize_key_value(value)
66
+ return value
67
+ end
68
+
69
+ #
70
+ # range generation provided for methods like created_infrastructure that need a set of partition key values
71
+ # to operate on.
72
+ #
73
+ def self.partition_generate_range(start_value, end_value, step = 1)
74
+ return Range.new(start_value, end_value).step(step)
75
+ end
76
+
77
+ #
78
+ # return an instance of this partition table's table manager.
79
+ #
80
+ def self.partition_manager
81
+ @partition_manager = self::PartitionManager.new(self) unless @partition_manager.present?
82
+ return @partition_manager
83
+ end
84
+
85
+ #
86
+ # return an instance of this partition table's sql_adapter (used by the partition manage to
87
+ # create SQL statements)
88
+ #
89
+ def self.sql_adapter
90
+ @sql_adapter = self::SqlAdapter.new(self) unless @sql_adapter.present?
91
+ return @sql_adapter
92
+ end
93
+
94
+ #
95
+ # in activerecord 3.0 we need to supply an Arel::Table for the key value(s) used
96
+ # to determine the specific child table to access.
97
+ #
98
+ def self.dynamic_arel_table(values, as = nil)
99
+ @arel_tables ||= {}
100
+ key_values = self.partition_key_values(values)
101
+ new_arel_table = @arel_tables[key_values]
102
+ arel_engine_hash = {:engine => self.arel_engine}
103
+ arel_engine_hash[:as] = as unless as.blank?
104
+ new_arel_table = Arel::Table.new(self.partition_name(*key_values), arel_engine_hash)
105
+ return new_arel_table
106
+ end
107
+
108
+ #
109
+ # used by our active record hacks to supply an Arel::Table given this active record's
110
+ # current attributes.
111
+ #
112
+ def dynamic_arel_table(as = nil)
113
+ symbolized_attributes = attributes.symbolize_keys
114
+ key_values = Hash[*self.class.partition_keys.map{|name| [name,symbolized_attributes[name]]}.flatten]
115
+ return self.class.dynamic_arel_table(key_values, as)
116
+ end
117
+
118
+ # :from_partition_scope is generally not used directly,
119
+ # use helper self.from_partition so that the derived class
120
+ # can be passed into :from_partition_scope
121
+ scope :from_partition_scope, lambda { |target_class, *partition_field|
122
+ {
123
+ :from => "#{target_class.partition_name(*partition_field)} AS #{target_class.table_name}"
124
+ }
125
+ }
126
+
127
+ #
128
+ # real scope (uses #from_partition_scope). This scope is used to target the
129
+ # active record find() to a specific child table and alias it to the name of the
130
+ # parent table (so activerecord can generally work with it)
131
+ #
132
+ # Use as:
133
+ #
134
+ # Foo.from_partition(KEY).find(:first)
135
+ #
136
+ # where KEY is the key value(s) used as the check constraint on Foo's table.
137
+ #
138
+ # Because the scope is specific to a class (a class method) but unlike
139
+ # class methods is not inherited, one must use this form (#from_partition) instead
140
+ # of #from_partition_scope to get the most derived classes specific active record scope.
141
+ #
142
+ def self.from_partition(*partition_field)
143
+ from_partition_scope(self, *partition_field)
144
+ end
145
+
146
+ # :from_partitioned_without_alias_scope is generally not used directly,
147
+ # use helper self.from_partitioned_without_alias so that the derived class
148
+ # can be passed into :from_partitioned_without_alias_scope
149
+ scope :from_partitioned_without_alias_scope, lambda { |target_class, *partition_field|
150
+ {
151
+ :from => target_class.partition_name(*partition_field)
152
+ }
153
+ }
154
+
155
+ #
156
+ # real scope (uses #from_partitioned_without_alias_scope). This scope is used to target the
157
+ # active record find() to a specific child table. Is probably best used in advanced
158
+ # activerecord queries when a number of tables are involved in the query.
159
+ #
160
+ # Use as:
161
+ #
162
+ # Foo.from_partitioned_without_alias(KEY).find(:all, :select => "*")
163
+ #
164
+ # where KEY is the key value(s) used as the check constraint on Foo's table.
165
+ #
166
+ # it's not obvious why :select => "*" is supplied. note activerecord wants
167
+ # to use the name of parent table for access to any attributes, so without
168
+ # the :select argument the sql result would be something like:
169
+ #
170
+ # SELECT foos.* FROM foos_partitions.pXXX
171
+ #
172
+ # which fails because table foos is not referenced. using the form #from_partition
173
+ # is almost always the correct thing when using activerecord.
174
+ #
175
+ # Because the scope is specific to a class (a class method) but unlike
176
+ # class methods is not inherited, one must use this form (#from_partitioned_without_alias) instead
177
+ # of #from_partitioned_without_alias_scope to get the most derived classes specific active record scope.
178
+ #
179
+ def self.from_partitioned_without_alias(*partition_field)
180
+ from_partitioned_without_alias_scope(self, *partition_field)
181
+ end
182
+
183
+ #
184
+ # return a object used to read configurator information
185
+ #
186
+ def self.configurator
187
+ unless @configurator
188
+ @configurator = self::Configurator::Reader.new(self)
189
+ end
190
+ return @configurator
191
+ end
192
+
193
+ #
194
+ # yields an object used to configure the ActiveRecord class for partitioning
195
+ # using the Configurator Domain Specific Language.
196
+ #
197
+ # usage:
198
+ # partitioned do |partition|
199
+ # partition.on :company_id
200
+ # partition.index :id, :unique => true
201
+ # partition.foreign_key :company_id
202
+ # end
203
+ #
204
+ def self.partitioned
205
+ @configurator_dsl ||= self::Configurator::Dsl.new(self)
206
+ yield @configurator_dsl
207
+ end
208
+
209
+ #
210
+ # returns the configurator DSL object
211
+ #
212
+ def self.configurator_dsl
213
+ return @configurator_dsl
214
+ end
215
+
216
+ partitioned do |partition|
217
+ #
218
+ # the schema name to place all child tables.
219
+ #
220
+ # by default this will be the table name of the parent class with a suffix "_partitions".
221
+ # for a parent table name foos, that would be foos_partitions
222
+ #
223
+ partition.schema_name lambda {|model|
224
+ return model.table_name + '_partitions'
225
+ }
226
+
227
+ #
228
+ # the table name of the table who is the direct ancestor of a child table.
229
+ # The child table is defined by the partition key values passed in.
230
+ #
231
+ # By default this is just the active record's notion of the name of the class.
232
+ # Multi Level partitiong requires more work.
233
+ #
234
+ partition.parent_table_name lambda {|model, *partition_key_values|
235
+ return model.table_name
236
+ }
237
+
238
+ #
239
+ # the schema name of the table who is the direct ancestor of a child table.
240
+ # The child table is defined by the partition key values passed in.
241
+ #
242
+ partition.parent_table_schema_name lambda {|model, *partition_key_values|
243
+ # this should be a connection_adapter thing
244
+ return "public"
245
+ }
246
+
247
+ #
248
+ # the prefix for a child table's name. This is typically a letter ('p') so that
249
+ # the base_name of the table can be a number generated programtically from
250
+ # the partition key values.
251
+ #
252
+ # for instance, a child table of the table 'foos' may be partitioned on
253
+ # the column company_id whose value is 42. specific child table would be named
254
+ # 'foos_partitions.p42'
255
+ #
256
+ # the 'p' is the name_prefix because 'foos_partitions.42' is not a valid table name
257
+ # (without quoting).
258
+ #
259
+ partition.name_prefix lambda {|model, *partition_key_values|
260
+ return "p"
261
+ }
262
+
263
+ #
264
+ # the child tables name without the schema name
265
+ #
266
+ partition.part_name lambda {|model, *partition_key_values|
267
+ configurator = model.configurator
268
+ return "#{configurator.name_prefix}#{configurator.base_name(*partition_key_values)}"
269
+ }
270
+
271
+ #
272
+ # the full name of a child table defined by the partition key values
273
+ #
274
+ partition.table_name lambda {|model, *partition_key_values|
275
+ configurator = model.configurator
276
+ return "#{configurator.schema_name}.#{configurator.part_name(*partition_key_values)}"
277
+ }
278
+
279
+ #
280
+ # the name of the child table without a schema name or prefix. this is used to
281
+ # build child table names for multi-level partitions.
282
+ #
283
+ # for a table named foos_partitions.p42, this would be "42"
284
+ #
285
+ partition.base_name lambda { |model, *partition_key_values|
286
+ return model.partition_normalize_key_value(*partition_key_values).to_s
287
+ }
288
+ end
289
+
290
+ ##
291
+ # :singleton-method: drop_partition_table
292
+ # delegated to Partitioned::PartitionedBase::PartitionManager#drop_partition_table
293
+
294
+ ##
295
+ # :singleton-method: create_partition_table
296
+ # delegated to Partitioned::PartitionedBase::PartitionManager#create_partition_table
297
+
298
+ ##
299
+ # :singleton-method: add_partition_table_index
300
+ # delegated to Partitioned::PartitionedBase::PartitionManager#add_partition_table_index
301
+
302
+ ##
303
+ # :singleton-method: add_references_to_partition_table
304
+ # delegated to Partitioned::PartitionedBase::PartitionManager#add_references_to_partition_table
305
+
306
+ ##
307
+ # :method: create_partition_schema
308
+ # delegated to Partitioned::PartitionedBase::PartitionManager#create_partition_schema
309
+
310
+ ##
311
+ # :singleton-method: add_parent_table_rules
312
+ # delegated to Partitioned::PartitionedBase::PartitionManager#add_parent_table_rules
313
+
314
+ ##
315
+ # :method: drop_old_partitions
316
+ # delegated to Partitioned::PartitionedBase::PartitionManager#drop_old_partitions
317
+
318
+ ##
319
+ # :method: create_new_partitions
320
+ # delegated to Partitioned::PartitionedBase::PartitionManager#create_new_partitions
321
+
322
+ ##
323
+ # :method: drop_old_partition
324
+ # delegated to Partitioned::PartitionedBase::PartitionManager#drop_old_partition
325
+
326
+ ##
327
+ # :method: create_new_partition
328
+ # delegated to Partitioned::PartitionedBase::PartitionManager#create_new_partition
329
+
330
+ ##
331
+ # :method: create_new_partition_tables
332
+ # delegated to Partitioned::PartitionedBase::PartitionManager#create_new_partition_tables
333
+
334
+ ##
335
+ # :method: create_infrastructure
336
+ # delegated to Partitioned::PartitionedBase::PartitionManager#create_infrastructure
337
+
338
+ ##
339
+ # :method: partition_table_name
340
+ # delegated to Partitioned::PartitionedBase::PartitionManager#partition_table_name
341
+
342
+ ##
343
+ # :method: partition_name
344
+ # delegated to Partitioned::PartitionedBase::PartitionManager#partition_table_name
345
+
346
+ extend SingleForwardable
347
+ def_delegators :partition_manager, :drop_partition_table, :create_partition_table,
348
+ :add_partition_table_index, :add_references_to_partition_table,
349
+ :create_partition_schema, :add_parent_table_rules, :drop_old_partitions,
350
+ :create_new_partitions, :drop_old_partition, :create_new_partition,
351
+ :create_new_partition_tables, :create_infrastructure, :partition_table_name
352
+ def_delegator :partition_manager, :partition_table_name, :partition_name
353
+ end
354
+ end
@@ -0,0 +1,6 @@
1
+ module Partitioned
2
+ class PartitionedBase < ActiveRecord::Base
3
+ module Configurator
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,62 @@
1
+ module Partitioned
2
+ class PartitionedBase
3
+ module Configurator
4
+ #
5
+ # the state configured by the Dsl and read by Reader
6
+ #
7
+ class Data
8
+ class Index
9
+ attr_accessor :field, :options
10
+ def initialize(field, options = {})
11
+ @field = field
12
+ @options = options
13
+ end
14
+ end
15
+ class ForeignKey
16
+ attr_accessor :referencing_field, :referenced_table, :referenced_field
17
+ def initialize(referencing_field, referenced_table = nil, referenced_field = :id)
18
+ @referencing_field = referencing_field
19
+ @referenced_table = if referenced_table.nil?
20
+ self.class.foreign_key_to_foreign_table_name(referencing_field)
21
+ else
22
+ referenced_table
23
+ end
24
+ @referenced_field = referenced_field
25
+ end
26
+
27
+ def self.foreign_key_to_foreign_table_name(foreign_key_field)
28
+ return ActiveSupport::Inflector::pluralize(foreign_key_field.to_s.sub(/_id$/,''))
29
+ end
30
+ end
31
+
32
+ attr_accessor :on_field, :indexes, :foreign_keys, :last_partitions_order_by_clause,
33
+ :schema_name, :name_prefix, :base_name,
34
+ :part_name, :table_name, :parent_table_schema_name,
35
+ :parent_table_name, :check_constraint, :encoded_name
36
+
37
+ def initialize
38
+ @on_field = nil
39
+ @indexes = []
40
+ @foreign_keys = []
41
+ @last_partitions_order_by_clause = nil
42
+
43
+ @schema_name = nil
44
+
45
+ @name_prefix = nil
46
+ @base_name = nil
47
+
48
+ @part_name = nil
49
+
50
+ @table_name = nil
51
+
52
+ @parent_table_schema_name = nil
53
+ @parent_table_name = nil
54
+
55
+ @check_constraint = nil
56
+
57
+ @encoded_name = nil
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end