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.
- data/Gemfile +17 -0
- data/LICENSE +30 -0
- data/PARTITIONING_EXPLAINED.txt +351 -0
- data/README +111 -0
- data/Rakefile +27 -0
- data/examples/README +23 -0
- data/examples/company_id.rb +417 -0
- data/examples/company_id_and_created_at.rb +689 -0
- data/examples/created_at.rb +590 -0
- data/examples/created_at_referencing_awards.rb +1000 -0
- data/examples/id.rb +475 -0
- data/examples/lib/by_company_id.rb +11 -0
- data/examples/lib/command_line_tool_mixin.rb +71 -0
- data/examples/lib/company.rb +29 -0
- data/examples/lib/get_options.rb +44 -0
- data/examples/lib/roman.rb +41 -0
- data/examples/start_date.rb +621 -0
- data/init.rb +1 -0
- data/lib/monkey_patch_activerecord.rb +92 -0
- data/lib/monkey_patch_postgres.rb +73 -0
- data/lib/partitioned.rb +26 -0
- data/lib/partitioned/active_record_overrides.rb +34 -0
- data/lib/partitioned/bulk_methods_mixin.rb +288 -0
- data/lib/partitioned/by_created_at.rb +13 -0
- data/lib/partitioned/by_foreign_key.rb +21 -0
- data/lib/partitioned/by_id.rb +35 -0
- data/lib/partitioned/by_integer_field.rb +32 -0
- data/lib/partitioned/by_monthly_time_field.rb +23 -0
- data/lib/partitioned/by_time_field.rb +65 -0
- data/lib/partitioned/by_weekly_time_field.rb +30 -0
- data/lib/partitioned/multi_level.rb +20 -0
- data/lib/partitioned/multi_level/configurator/data.rb +14 -0
- data/lib/partitioned/multi_level/configurator/dsl.rb +32 -0
- data/lib/partitioned/multi_level/configurator/reader.rb +162 -0
- data/lib/partitioned/multi_level/partition_manager.rb +47 -0
- data/lib/partitioned/partitioned_base.rb +354 -0
- data/lib/partitioned/partitioned_base/configurator.rb +6 -0
- data/lib/partitioned/partitioned_base/configurator/data.rb +62 -0
- data/lib/partitioned/partitioned_base/configurator/dsl.rb +628 -0
- data/lib/partitioned/partitioned_base/configurator/reader.rb +209 -0
- data/lib/partitioned/partitioned_base/partition_manager.rb +138 -0
- data/lib/partitioned/partitioned_base/sql_adapter.rb +286 -0
- data/lib/partitioned/version.rb +3 -0
- data/lib/tasks/desirable_tasks.rake +4 -0
- data/partitioned.gemspec +21 -0
- data/spec/dummy/.rspec +1 -0
- data/spec/dummy/README.rdoc +261 -0
- data/spec/dummy/Rakefile +7 -0
- data/spec/dummy/app/assets/javascripts/application.js +9 -0
- data/spec/dummy/app/assets/stylesheets/application.css +7 -0
- data/spec/dummy/app/controllers/application_controller.rb +3 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +51 -0
- data/spec/dummy/config/boot.rb +10 -0
- data/spec/dummy/config/database.yml +32 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +30 -0
- data/spec/dummy/config/environments/production.rb +60 -0
- data/spec/dummy/config/environments/test.rb +39 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/inflections.rb +10 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +7 -0
- data/spec/dummy/config/initializers/session_store.rb +8 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +5 -0
- data/spec/dummy/config/routes.rb +58 -0
- data/spec/dummy/public/404.html +26 -0
- data/spec/dummy/public/422.html +26 -0
- data/spec/dummy/public/500.html +26 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/script/rails +6 -0
- data/spec/dummy/spec/spec_helper.rb +27 -0
- data/spec/monkey_patch_posgres_spec.rb +176 -0
- data/spec/partitioned/bulk_methods_mixin_spec.rb +512 -0
- data/spec/partitioned/by_created_at_spec.rb +62 -0
- data/spec/partitioned/by_foreign_key_spec.rb +95 -0
- data/spec/partitioned/by_id_spec.rb +97 -0
- data/spec/partitioned/by_integer_field_spec.rb +143 -0
- data/spec/partitioned/by_monthly_time_field_spec.rb +100 -0
- data/spec/partitioned/by_time_field_spec.rb +182 -0
- data/spec/partitioned/by_weekly_time_field_spec.rb +100 -0
- data/spec/partitioned/multi_level/configurator/dsl_spec.rb +88 -0
- data/spec/partitioned/multi_level/configurator/reader_spec.rb +147 -0
- data/spec/partitioned/partitioned_base/configurator/dsl_spec.rb +459 -0
- data/spec/partitioned/partitioned_base/configurator/reader_spec.rb +513 -0
- data/spec/partitioned/partitioned_base/sql_adapter_spec.rb +204 -0
- data/spec/partitioned/partitioned_base_spec.rb +173 -0
- data/spec/spec_helper.rb +32 -0
- data/spec/support/shared_example_spec_helper_for_integer_key.rb +137 -0
- data/spec/support/shared_example_spec_helper_for_time_key.rb +147 -0
- data/spec/support/tables_spec_helper.rb +47 -0
- metadata +250 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
module Partitioned
|
|
2
|
+
class PartitionedBase
|
|
3
|
+
module Configurator
|
|
4
|
+
class Reader
|
|
5
|
+
attr_reader :model
|
|
6
|
+
|
|
7
|
+
def initialize(most_derived_activerecord_class)
|
|
8
|
+
@model = most_derived_activerecord_class
|
|
9
|
+
@configurators = nil
|
|
10
|
+
@on_fields = nil
|
|
11
|
+
@indexes = nil
|
|
12
|
+
@foreign_keys = nil
|
|
13
|
+
@check_constraint = nil
|
|
14
|
+
|
|
15
|
+
@schema_name = nil
|
|
16
|
+
@name_prefix = nil
|
|
17
|
+
@base_name = nil
|
|
18
|
+
@part_name = nil
|
|
19
|
+
|
|
20
|
+
@table_name = nil
|
|
21
|
+
|
|
22
|
+
@parent_table_schema_name = nil
|
|
23
|
+
@parent_table_name = nil
|
|
24
|
+
|
|
25
|
+
@encoded_name = nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
#
|
|
29
|
+
# The name of the schema that will contain all child tables.
|
|
30
|
+
#
|
|
31
|
+
def schema_name
|
|
32
|
+
unless @schema_name
|
|
33
|
+
@schema_name = collect_first(&:schema_name)
|
|
34
|
+
end
|
|
35
|
+
return @schema_name
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
#
|
|
39
|
+
# The field used to partition child tables.
|
|
40
|
+
#
|
|
41
|
+
def on_fields
|
|
42
|
+
unless @on_fields
|
|
43
|
+
@on_fields = collect(&:on_field).map(&:to_sym)
|
|
44
|
+
end
|
|
45
|
+
return @on_fields
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
#
|
|
49
|
+
# Define an index to be created on all (leaf-) child tables.
|
|
50
|
+
#
|
|
51
|
+
def indexes(*partition_key_values)
|
|
52
|
+
return collect_from_collection(*partition_key_values, &:indexes).inject({}) do |bag, data_index|
|
|
53
|
+
bag[data_index.field] = (data_index.options || {}) unless data_index.blank?
|
|
54
|
+
bag
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
#
|
|
59
|
+
# Define a foreign key on a (leaf-) child table.
|
|
60
|
+
#
|
|
61
|
+
def foreign_keys(*partition_key_values)
|
|
62
|
+
return collect_from_collection(*partition_key_values, &:foreign_keys).inject(Set.new) do |set,new_items|
|
|
63
|
+
if new_items.is_a? Array
|
|
64
|
+
set += new_items
|
|
65
|
+
else
|
|
66
|
+
set += [new_items]
|
|
67
|
+
end
|
|
68
|
+
set
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
#
|
|
73
|
+
# Define the check constraint for a given child table.
|
|
74
|
+
#
|
|
75
|
+
def check_constraint(*partition_key_values)
|
|
76
|
+
return collect_first(*partition_key_values, &:check_constraint)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
#
|
|
80
|
+
# The table name of the table who is the direct ancestor of a child table.
|
|
81
|
+
#
|
|
82
|
+
def parent_table_name(*partition_key_values)
|
|
83
|
+
return collect_first(*partition_key_values, &:parent_table_name)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
#
|
|
87
|
+
# The schema name of the table who is the direct ancestor of a child table.
|
|
88
|
+
#
|
|
89
|
+
def parent_table_schema_name(*partition_key_values)
|
|
90
|
+
return collect_first(*partition_key_values, &:parent_table_schema_name)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
#
|
|
94
|
+
# The full name of a child table defined by the partition key values.
|
|
95
|
+
#
|
|
96
|
+
def table_name(*partition_key_values)
|
|
97
|
+
return collect_first(*partition_key_values, &:table_name)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
#
|
|
101
|
+
# The name of the child table without the schema name or name prefix.
|
|
102
|
+
#
|
|
103
|
+
def base_name(*partition_key_values)
|
|
104
|
+
return collect_first(*partition_key_values, &:base_name)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
#
|
|
108
|
+
# The prefix for the child table's name.
|
|
109
|
+
#
|
|
110
|
+
def name_prefix
|
|
111
|
+
unless @name_prefix
|
|
112
|
+
@name_prefix = collect_first(&:name_prefix)
|
|
113
|
+
end
|
|
114
|
+
return @name_prefix
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
#
|
|
118
|
+
# The child tables name without the schema name.
|
|
119
|
+
#
|
|
120
|
+
def part_name(*partition_key_values)
|
|
121
|
+
return collect_first(*partition_key_values, &:part_name)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
#
|
|
125
|
+
# Define the order by clause used to list all child table names in order
|
|
126
|
+
# of "last to be used" to "oldest to have been used".
|
|
127
|
+
#
|
|
128
|
+
def last_partitions_order_by_clause
|
|
129
|
+
unless @last_partitions_order_by_clause
|
|
130
|
+
@last_partitions_order_by_clause = collect_first(&:last_partitions_order_by_clause)
|
|
131
|
+
end
|
|
132
|
+
return @last_partitions_order_by_clause
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
protected
|
|
137
|
+
|
|
138
|
+
def configurators
|
|
139
|
+
unless @configurators
|
|
140
|
+
@configurators = []
|
|
141
|
+
model.ancestors.each do |ancestor|
|
|
142
|
+
if ancestor.respond_to?(:configurator_dsl)
|
|
143
|
+
if ancestor::configurator_dsl
|
|
144
|
+
@configurators << ancestor::configurator_dsl
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
break if ancestor == Partitioned::PartitionedBase
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
return @configurators
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def collect(*partition_key_values, &block)
|
|
154
|
+
values = []
|
|
155
|
+
configurators.each do |configurator|
|
|
156
|
+
data = configurator.data
|
|
157
|
+
|
|
158
|
+
intermediate_value = block.call(data) rescue nil
|
|
159
|
+
if intermediate_value.is_a? Proc
|
|
160
|
+
values << intermediate_value.call(model, *partition_key_values)
|
|
161
|
+
elsif intermediate_value.is_a? String
|
|
162
|
+
field_value = partition_key_values.first
|
|
163
|
+
values << eval("\"#{intermediate_value}\"")
|
|
164
|
+
else
|
|
165
|
+
values << intermediate_value unless intermediate_value.blank?
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
return values
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def collect_first(*partition_key_values, &block)
|
|
172
|
+
configurators.each do |configurator|
|
|
173
|
+
data = configurator.data
|
|
174
|
+
intermediate_value = block.call(data) rescue nil
|
|
175
|
+
if intermediate_value.is_a? Proc
|
|
176
|
+
return intermediate_value.call(model, *partition_key_values)
|
|
177
|
+
elsif intermediate_value.is_a? String
|
|
178
|
+
field_value = partition_key_values.first
|
|
179
|
+
return eval("\"#{intermediate_value}\"")
|
|
180
|
+
else
|
|
181
|
+
return intermediate_value unless intermediate_value.nil?
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
return nil
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def collect_from_collection(*partition_key_values, &block)
|
|
188
|
+
values = []
|
|
189
|
+
configurators.each do |configurator|
|
|
190
|
+
data = configurator.data
|
|
191
|
+
intermediate_values = []
|
|
192
|
+
intermediate_values = block.call(data) rescue nil
|
|
193
|
+
[*intermediate_values].each do |intermediate_value|
|
|
194
|
+
if intermediate_value.is_a? Proc
|
|
195
|
+
values << intermediate_value.call(model, *partition_key_values)
|
|
196
|
+
elsif intermediate_value.is_a? String
|
|
197
|
+
field_value = partition_key_values.first
|
|
198
|
+
values << eval("\"#{intermediate_value}\"")
|
|
199
|
+
else
|
|
200
|
+
values << intermediate_value unless intermediate_value.blank?
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
return values
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
module Partitioned
|
|
2
|
+
class PartitionedBase
|
|
3
|
+
#
|
|
4
|
+
# PartitionManager
|
|
5
|
+
# interface for all requests made to build partition tables.
|
|
6
|
+
# these are typically delegated to us from the ActiveRecord class
|
|
7
|
+
# (partitioned_base.rb defines the forwarding)
|
|
8
|
+
class PartitionManager
|
|
9
|
+
attr_reader :parent_table_class
|
|
10
|
+
|
|
11
|
+
def initialize(parent_table_class)
|
|
12
|
+
@parent_table_class = parent_table_class
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
#
|
|
16
|
+
# drop partitions that are no longer necessary.
|
|
17
|
+
# uses #old_partition_key_values_set as the list of
|
|
18
|
+
# partitions to remove.
|
|
19
|
+
#
|
|
20
|
+
def drop_old_partitions
|
|
21
|
+
old_partition_key_values_set.each do |*partition_key_values|
|
|
22
|
+
drop_old_partition(*partition_key_values)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
#
|
|
27
|
+
# create partitions that are needed (probably to handle data that
|
|
28
|
+
# will be inserted into the database within the next few weeks).
|
|
29
|
+
# uses #new_partition_key_value_set to determine the key values
|
|
30
|
+
# for the specific child tables to create.
|
|
31
|
+
#
|
|
32
|
+
def create_new_partitions
|
|
33
|
+
new_partition_key_values_set.each do |*partition_key_values|
|
|
34
|
+
create_new_partition(*partition_key_values)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
#
|
|
39
|
+
# create any partition tables from a list. the partition tables must
|
|
40
|
+
# not already exist and its schema must already exist.
|
|
41
|
+
#
|
|
42
|
+
def create_new_partition_tables(enumerable)
|
|
43
|
+
enumerable.each do |partition_key_values|
|
|
44
|
+
create_new_partition(*partition_key_values)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
#
|
|
49
|
+
# the once called function to prepare a parent table for partitioning as well
|
|
50
|
+
# as create the schema that the child tables will be placed in.
|
|
51
|
+
#
|
|
52
|
+
def create_infrastructure
|
|
53
|
+
create_partition_schema
|
|
54
|
+
add_parent_table_rules
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
protected
|
|
58
|
+
#
|
|
59
|
+
# an array of key values (each key value is an array of keys) that represent
|
|
60
|
+
# the child partitions that should be created.
|
|
61
|
+
#
|
|
62
|
+
# used by #create_new_partitions and generally called once a day to update
|
|
63
|
+
# the database with new soon-to-be needed child tables
|
|
64
|
+
#
|
|
65
|
+
# typically overridden by the concrete class as this is pure business logic
|
|
66
|
+
#
|
|
67
|
+
def new_partition_key_values_set
|
|
68
|
+
[]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
#
|
|
72
|
+
# an array of key values (each key value is an array of keys) that represent
|
|
73
|
+
# the child partitions that should be dropped because they are no longer needed.
|
|
74
|
+
#
|
|
75
|
+
# used by #drop_old_partitions and generally called once a day to clean up
|
|
76
|
+
# unneeded child tables
|
|
77
|
+
#
|
|
78
|
+
# typically overridden by the concrete class as this is pure business logic
|
|
79
|
+
#
|
|
80
|
+
def old_partition_key_values_set
|
|
81
|
+
[]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
#
|
|
85
|
+
# remove a specific partition from the database given
|
|
86
|
+
# the key value(s) of its check constraint columns
|
|
87
|
+
#
|
|
88
|
+
def drop_old_partition(*partition_key_values)
|
|
89
|
+
drop_partition_table(*partition_key_values)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
#
|
|
93
|
+
# create a specific child table that does not currently
|
|
94
|
+
# exist and whose schema (the schema that the table exists in)
|
|
95
|
+
# also already exists (#create_infrastructure is designed to
|
|
96
|
+
# create this).
|
|
97
|
+
#
|
|
98
|
+
def create_new_partition(*partition_key_values)
|
|
99
|
+
create_partition_table(*partition_key_values)
|
|
100
|
+
add_partition_table_index(*partition_key_values)
|
|
101
|
+
add_references_to_partition_table(*partition_key_values)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
##
|
|
105
|
+
# :method: drop_partition_table
|
|
106
|
+
# delegated to Partitioned::PartitionedBase::PartitionManager::SqlAdapter#drop_partition_table
|
|
107
|
+
|
|
108
|
+
##
|
|
109
|
+
# :method: create_partition_table
|
|
110
|
+
# delegated to Partitioned::PartitionedBase::PartitionManager::SqlAdapter#create_partition_table
|
|
111
|
+
|
|
112
|
+
##
|
|
113
|
+
# :method: add_partition_table_index
|
|
114
|
+
# delegated to Partitioned::PartitionedBase::PartitionManager::SqlAdapter#add_partition_table_index
|
|
115
|
+
|
|
116
|
+
##
|
|
117
|
+
# :method: add_references_to_partition_table
|
|
118
|
+
# delegated to Partitioned::PartitionedBase::PartitionManager::SqlAdapter#add_references_to_partition_table
|
|
119
|
+
|
|
120
|
+
##
|
|
121
|
+
# :method: create_partition_schema
|
|
122
|
+
# delegated to Partitioned::PartitionedBase::PartitionManager::SqlAdapter#create_partition_schema
|
|
123
|
+
|
|
124
|
+
##
|
|
125
|
+
# :method: add_parent_table_rules
|
|
126
|
+
# delegated to Partitioned::PartitionedBase::PartitionManager::SqlAdapter#add_parent_table_rules
|
|
127
|
+
|
|
128
|
+
##
|
|
129
|
+
# :method: partition_table_name
|
|
130
|
+
# delegated to Partitioned::PartitionedBase::PartitionManager::SqlAdapter#partition_table_name
|
|
131
|
+
|
|
132
|
+
extend Forwardable
|
|
133
|
+
def_delegators :parent_table_class, :sql_adapter
|
|
134
|
+
def_delegators :sql_adapter, :drop_partition_table, :create_partition_table, :add_partition_table_index,
|
|
135
|
+
:add_references_to_partition_table, :create_partition_schema, :add_parent_table_rules, :partition_table_name
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
module Partitioned
|
|
2
|
+
class PartitionedBase
|
|
3
|
+
#
|
|
4
|
+
# SqlAdapter
|
|
5
|
+
# manages requests of partitioned tables.
|
|
6
|
+
#
|
|
7
|
+
class SqlAdapter
|
|
8
|
+
attr_reader :parent_table_class
|
|
9
|
+
|
|
10
|
+
def initialize(parent_table_class)
|
|
11
|
+
@parent_table_class = parent_table_class
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
#
|
|
15
|
+
# ensure our function for warning about improper partition usage is in place
|
|
16
|
+
#
|
|
17
|
+
# Name: always_fail_on_insert(text); Type: FUNCTION; Schema: public
|
|
18
|
+
#
|
|
19
|
+
# Used to raise an exception explaining why a specific insert (into a parent
|
|
20
|
+
# table which should never have records) should never be attempted.
|
|
21
|
+
#
|
|
22
|
+
def ensure_always_fail_on_insert_exists
|
|
23
|
+
sql = <<-SQL
|
|
24
|
+
CREATE OR REPLACE FUNCTION always_fail_on_insert(table_name text) RETURNS boolean
|
|
25
|
+
LANGUAGE plpgsql
|
|
26
|
+
AS $$
|
|
27
|
+
BEGIN
|
|
28
|
+
RAISE EXCEPTION 'partitioned table "%" does not support direct inserts, you should be inserting directly into child tables', table_name;
|
|
29
|
+
RETURN false;
|
|
30
|
+
END;
|
|
31
|
+
$$;
|
|
32
|
+
SQL
|
|
33
|
+
execute(sql)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
#
|
|
37
|
+
# Child tables whose parent table is 'foos', typically exist in a schema named foos_partitions.
|
|
38
|
+
#
|
|
39
|
+
# *partition_key_values are needed here to support the use of multiple schemas to keep tables in.
|
|
40
|
+
#
|
|
41
|
+
def create_partition_schema(*partition_key_values)
|
|
42
|
+
create_schema(configurator.schema_name, :unless_exists => true)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
#
|
|
46
|
+
# does a specific child partition exist
|
|
47
|
+
#
|
|
48
|
+
def partition_exists?(*partition_key_values)
|
|
49
|
+
return find(:first,
|
|
50
|
+
:from => "pg_tables",
|
|
51
|
+
:select => "count(*) as count",
|
|
52
|
+
:conditions => ["schemaname = ? and tablename = ?",
|
|
53
|
+
configurator.schema_name,
|
|
54
|
+
configurator.part_name(*partition_key_values)
|
|
55
|
+
]).count.to_i == 1
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
#
|
|
59
|
+
# returns an array of partition table names from last to first limited to
|
|
60
|
+
# the number of entries requested by its first parameter.
|
|
61
|
+
#
|
|
62
|
+
# the magic here is in the overridden method "last_n_partitions_order_by_clause"
|
|
63
|
+
# which is designed to order a list of partition table names (table names without
|
|
64
|
+
# their schema name) from last to first.
|
|
65
|
+
#
|
|
66
|
+
# if the child table names are the format "pYYYYMMDD" where YYYY is a four digit year, MM is
|
|
67
|
+
# a month number and DD is a day number, you would use the following to order from last to
|
|
68
|
+
# first:
|
|
69
|
+
# tablename desc
|
|
70
|
+
#
|
|
71
|
+
# for child table names of the format "pXXXX" where XXXX is a number, you may want something like:
|
|
72
|
+
# substring(tablename, 2)::integer desc
|
|
73
|
+
#
|
|
74
|
+
# for clarity, the sql executed is:
|
|
75
|
+
# select tablename from pg_tables where schemaname = $1 order by $2 limit $3
|
|
76
|
+
# where:
|
|
77
|
+
# $1 = the name of schema (foos_partitions)
|
|
78
|
+
# $2 = the order by clause that would make the greatest table name listed first
|
|
79
|
+
# $3 = the parameter 'how_many'
|
|
80
|
+
#
|
|
81
|
+
def last_n_partition_names(how_many = 1)
|
|
82
|
+
return find(:all,
|
|
83
|
+
:from => "pg_tables",
|
|
84
|
+
:select => :tablename,
|
|
85
|
+
:conditions => ["schemaname = ?", configurator.schema_name],
|
|
86
|
+
:order => last_n_partitions_order_by_clause,
|
|
87
|
+
:limit => how_many).map(&:tablename)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
#
|
|
91
|
+
# override this or order the tables from last (greatest value? greatest date?) to first
|
|
92
|
+
#
|
|
93
|
+
def last_n_partitions_order_by_clause
|
|
94
|
+
return configurator.last_n_partitions_order_by_clause
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
#
|
|
98
|
+
# used to create the parent table rule to ensure
|
|
99
|
+
#
|
|
100
|
+
# this will cause an error on attempt to insert into the parent table
|
|
101
|
+
#
|
|
102
|
+
# we want all records to exist in one of the child tables so the
|
|
103
|
+
# query planner can optimize access to the records.
|
|
104
|
+
#
|
|
105
|
+
def add_parent_table_rules(*partition_key_values)
|
|
106
|
+
ensure_always_fail_on_insert_exists
|
|
107
|
+
|
|
108
|
+
insert_redirector_name = parent_table_rule_name("insert", "redirector", *partition_key_values)
|
|
109
|
+
sql = <<-SQL
|
|
110
|
+
CREATE OR REPLACE RULE #{insert_redirector_name} AS
|
|
111
|
+
ON INSERT TO #{configurator.parent_table_name(*partition_key_values)}
|
|
112
|
+
DO INSTEAD
|
|
113
|
+
(
|
|
114
|
+
SELECT always_fail_on_insert('#{configurator.parent_table_name(*partition_key_values)}')
|
|
115
|
+
)
|
|
116
|
+
SQL
|
|
117
|
+
execute(sql)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
#
|
|
121
|
+
# the name of the table (schemaname.childtablename) given the check constraint values.
|
|
122
|
+
#
|
|
123
|
+
def partition_table_name(*partition_key_values)
|
|
124
|
+
return configurator.table_name(*partition_key_values)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
#
|
|
128
|
+
# create a single child table.
|
|
129
|
+
#
|
|
130
|
+
def create_partition_table(*partition_key_values)
|
|
131
|
+
create_table(configurator.table_name(*partition_key_values), {
|
|
132
|
+
:id => false,
|
|
133
|
+
:options => "INHERITS (#{configurator.parent_table_name(*partition_key_values)})"
|
|
134
|
+
}) do |t|
|
|
135
|
+
t.check_constraint configurator.check_constraint(*partition_key_values)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
#
|
|
140
|
+
# remove a specific single child table
|
|
141
|
+
#
|
|
142
|
+
def drop_partition_table(*partition_key_values)
|
|
143
|
+
drop_table(configurator.table_name(*partition_key_values))
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
#
|
|
147
|
+
# add indexes that must exist on child tables. Only leaf child tables
|
|
148
|
+
# need indexes as parent table indexes are not used in postgres.
|
|
149
|
+
#
|
|
150
|
+
def add_partition_table_index(*partition_key_values)
|
|
151
|
+
configurator.indexes(*partition_key_values).each do |field,options|
|
|
152
|
+
used_options = options.clone
|
|
153
|
+
unless used_options.has_key?(:name)
|
|
154
|
+
name = [*field].join('_')
|
|
155
|
+
used_options[:name] = used_options[:unique] ? unique_index_name(name, *partition_key_values) : index_name(name, *partition_key_values)
|
|
156
|
+
end
|
|
157
|
+
add_index(partition_table_name(*partition_key_values), field, used_options)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
#
|
|
162
|
+
# used when creating the name of a SQL rule
|
|
163
|
+
#
|
|
164
|
+
def parent_table_rule_name(name, suffix = "rule", *partition_key_values)
|
|
165
|
+
return "#{configurator.parent_table_name(*partition_key_values).gsub(/[.]/, '_')}_#{name}_#{suffix}"
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
#
|
|
169
|
+
# used to create index names
|
|
170
|
+
#
|
|
171
|
+
def index_name(name, *partition_key_values)
|
|
172
|
+
return "#{configurator.part_name(*partition_key_values)}_#{name}_idx"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
#
|
|
176
|
+
# used to create index names
|
|
177
|
+
#
|
|
178
|
+
def unique_index_name(name, *partition_key_values)
|
|
179
|
+
return "#{configurator.part_name(*partition_key_values)}_#{name}_udx"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
#
|
|
183
|
+
# this is here for derived classes to set up references to added columns
|
|
184
|
+
# (or columns in the parent that need foreign key constraints).
|
|
185
|
+
#
|
|
186
|
+
# foreign keys are not inherited in postgres. So, a parent table
|
|
187
|
+
# of the form:
|
|
188
|
+
#
|
|
189
|
+
# -- this is the referenced table
|
|
190
|
+
# create table companies
|
|
191
|
+
# (
|
|
192
|
+
# id serial not null primary key,
|
|
193
|
+
# created_at timestamp not null default now(),
|
|
194
|
+
# updated_at timestamp,
|
|
195
|
+
# name text not null
|
|
196
|
+
# );
|
|
197
|
+
#
|
|
198
|
+
# -- this is the parent table
|
|
199
|
+
# create table employees
|
|
200
|
+
# (
|
|
201
|
+
# id serial not null primary key,
|
|
202
|
+
# created_at timestamp not null default now(),
|
|
203
|
+
# updated_at timestamp,
|
|
204
|
+
# name text not null,
|
|
205
|
+
# company_id integer not null references companies,
|
|
206
|
+
# supervisor_id integer not null references employees
|
|
207
|
+
# );
|
|
208
|
+
#
|
|
209
|
+
# -- some children
|
|
210
|
+
# create table employees_of_company_1 ( CHECK ( company_id = 1 ) ) INHERITS (employees);
|
|
211
|
+
# create table employees_of_company_2 ( CHECK ( company_id = 2 ) ) INHERITS (employees);
|
|
212
|
+
# create table employees_of_company_3 ( CHECK ( company_id = 3 ) ) INHERITS (employees);
|
|
213
|
+
#
|
|
214
|
+
# since postgres does not inherit referential integrity from parent tables, the following
|
|
215
|
+
# insert will work:
|
|
216
|
+
# insert into employees_of_company_1 (name, company_id, supervisor_id) values ('joe', 1, 10);
|
|
217
|
+
# even if there is no record in companies with id = 1 and there is no record in employees with id = 10
|
|
218
|
+
#
|
|
219
|
+
# for proper referential integrity handling you must do the following:
|
|
220
|
+
# ALTER TABLE employees_of_company_1 add foreign key (company_id) references companies(id)
|
|
221
|
+
# ALTER TABLE employees_of_company_2 add foreign key (company_id) references companies(id)
|
|
222
|
+
# ALTER TABLE employees_of_company_3 add foreign key (company_id) references companies(id)
|
|
223
|
+
#
|
|
224
|
+
# ALTER TABLE employees_of_company_1 add foreign key (supervisor_id) references employees_of_company_1(id)
|
|
225
|
+
# ALTER TABLE employees_of_company_2 add foreign key (supervisor_id) references employees_of_company_2(id)
|
|
226
|
+
# ALTER TABLE employees_of_company_3 add foreign key (supervisor_id) references employees_of_company_3(id)
|
|
227
|
+
#
|
|
228
|
+
# the second set of alter tables brings up a good another consideration about postgres references and partitions.
|
|
229
|
+
# postgres will not follow references to a child table. So, a foreign key reference to "employees" in this
|
|
230
|
+
# set of alter statements would not work because postgres would expect the table "employees" to have
|
|
231
|
+
# the specific referenced record, but the record really exists in a child of employees. So, the alter statement
|
|
232
|
+
# forces the reference check on the specific child table we know must contain this employees supervisor (since
|
|
233
|
+
# such a supervisor would have to work for the same company in our model).
|
|
234
|
+
#
|
|
235
|
+
def add_references_to_partition_table(*partition_key_values)
|
|
236
|
+
configurator.foreign_keys(*partition_key_values).each do |foreign_key|
|
|
237
|
+
add_foreign_key(partition_table_name(*partition_key_values),
|
|
238
|
+
foreign_key.referencing_field,
|
|
239
|
+
foreign_key.referenced_table,
|
|
240
|
+
foreign_key.referenced_field)
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
##
|
|
245
|
+
# :method: connection
|
|
246
|
+
# delegated to the connection of the parent table class
|
|
247
|
+
|
|
248
|
+
##
|
|
249
|
+
# :method: execute
|
|
250
|
+
# delegated to the connection of the parent table class
|
|
251
|
+
|
|
252
|
+
##
|
|
253
|
+
# :method: create_schema
|
|
254
|
+
# delegated to the connection of the parent table class
|
|
255
|
+
|
|
256
|
+
##
|
|
257
|
+
# :method: drop_schema
|
|
258
|
+
# delegated to the connection of the parent table class
|
|
259
|
+
|
|
260
|
+
##
|
|
261
|
+
# :method: add_index
|
|
262
|
+
# delegated to the connection of the parent table class
|
|
263
|
+
|
|
264
|
+
##
|
|
265
|
+
# :method: remove_index
|
|
266
|
+
# delegated to the connection of the parent table class
|
|
267
|
+
|
|
268
|
+
##
|
|
269
|
+
# :method: transaction
|
|
270
|
+
# delegated to the connection of the parent table class
|
|
271
|
+
|
|
272
|
+
##
|
|
273
|
+
# :method: find_by_sql
|
|
274
|
+
# delegated to the connection of the parent table class
|
|
275
|
+
|
|
276
|
+
##
|
|
277
|
+
# :method: find
|
|
278
|
+
# delegated to the connection of the parent table class
|
|
279
|
+
|
|
280
|
+
extend Forwardable
|
|
281
|
+
def_delegators :parent_table_class, :connection, :find_by_sql, :transaction, :find, :configurator
|
|
282
|
+
def_delegators :connection, :execute, :add_index, :remove_index, :create_schema, :drop_schema, :add_foreign_key,
|
|
283
|
+
:create_table, :drop_table
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|