partitioned 0.8.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 (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