partitioned 1.3.5 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +4 -0
- data/.travis.yml +1 -2
- data/lib/monkey_patch_activerecord.rb +115 -81
- data/lib/monkey_patch_postgres.rb +0 -13
- data/lib/partitioned/by_time_field.rb +2 -1
- data/lib/partitioned/partitioned_base.rb +2 -2
- data/lib/partitioned/partitioned_base/redshift_sql_adapter.rb +5 -7
- data/lib/partitioned/partitioned_base/sql_adapter.rb +31 -15
- data/lib/partitioned/version.rb +1 -1
- data/partitioned.gemspec +4 -4
- data/spec/dummy/.gitignore +16 -0
- data/spec/dummy/README.rdoc +15 -248
- data/spec/dummy/Rakefile +0 -1
- data/spec/dummy/app/assets/javascripts/application.js +11 -4
- data/spec/dummy/app/assets/stylesheets/application.css +11 -5
- data/spec/dummy/app/controllers/application_controller.rb +3 -1
- data/spec/dummy/app/views/layouts/application.html.erb +2 -2
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/config.ru +1 -1
- data/spec/dummy/config/application.rb +6 -32
- data/spec/dummy/config/boot.rb +3 -9
- data/spec/dummy/config/environment.rb +2 -2
- data/spec/dummy/config/environments/development.rb +12 -13
- data/spec/dummy/config/environments/production.rb +44 -24
- data/spec/dummy/config/environments/test.rb +15 -18
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +9 -3
- data/spec/dummy/config/initializers/secret_token.rb +7 -2
- data/spec/dummy/config/initializers/session_store.rb +1 -6
- data/spec/dummy/config/initializers/wrap_parameters.rb +6 -6
- data/spec/dummy/config/locales/en.yml +20 -2
- data/spec/dummy/config/routes.rb +22 -24
- data/spec/dummy/db/seeds.rb +7 -0
- data/spec/dummy/public/404.html +43 -11
- data/spec/dummy/public/422.html +43 -11
- data/spec/dummy/public/500.html +43 -12
- data/spec/dummy/public/robots.txt +5 -0
- data/spec/dummy/test/test_helper.rb +15 -0
- data/spec/{monkey_patch_posgres_spec.rb → monkey_patch_postgres_spec.rb} +0 -13
- data/spec/partitioned/by_created_at_spec.rb +1 -2
- data/spec/partitioned/by_daily_time_field_spec.rb +0 -1
- data/spec/partitioned/by_foreign_key_spec.rb +0 -1
- data/spec/partitioned/by_id_spec.rb +16 -3
- data/spec/partitioned/by_integer_field_spec.rb +0 -1
- data/spec/partitioned/by_monthly_time_field_spec.rb +1 -2
- data/spec/partitioned/by_time_field_spec.rb +1 -2
- data/spec/partitioned/by_weekly_time_field_spec.rb +0 -1
- data/spec/partitioned/by_yearly_time_field_spec.rb +1 -2
- data/spec/partitioned/partitioned_base/sql_adapter_spec.rb +2 -2
- data/spec/partitioned/partitioned_base_spec.rb +1 -1
- data/spec/support/shared_example_spec_helper_for_integer_key.rb +2 -2
- data/spec/support/shared_example_spec_helper_for_time_key.rb +1 -1
- data/spec/support/tables_spec_helper.rb +2 -2
- data/travis/before.sh +6 -0
- metadata +29 -16
- data/spec/dummy/script/rails +0 -6
- data/spec/dummy/spec/spec_helper.rb +0 -27
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: be864b7e40e4e6c504bf98b195a564bde6d076b4
|
4
|
+
data.tar.gz: b79141c1dc5742385d9426f7a6fcf176844642ec
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b0a74962101fbff55ae9edc9a8203088fca7aaa078b3bbac3594a03d3d3c8bb122f545cb984c5ad5a897ffbe80c557d530180cd2b0a0af5e353ba0bb469adcdc
|
7
|
+
data.tar.gz: 06c0780a190219771c06f27091774395eb682e126bf458dcb4cc1091a41b6ece5e3d104ed343684eae41e50259c6e7fc167e7ee40b9833b0460d16e036f7d2bb
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
@@ -14,101 +14,135 @@ module ActiveRecord
|
|
14
14
|
# Patches for Persistence to allow certain partitioning (that related to the primary key) to work.
|
15
15
|
#
|
16
16
|
module Persistence
|
17
|
-
#
|
18
|
-
#
|
19
|
-
def
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
relation = self.class.unscoped.where(using_arel_table[pk].eq(substitute))
|
35
|
-
end
|
36
|
-
|
37
|
-
relation.bind_values = [[column, id]]
|
38
|
-
relation.delete_all
|
17
|
+
# This method is patched to provide a relation referencing the partition instead
|
18
|
+
# of the parent table.
|
19
|
+
def relation_for_destroy
|
20
|
+
pk = self.class.primary_key
|
21
|
+
column = self.class.columns_hash[pk]
|
22
|
+
substitute = self.class.connection.substitute_at(column, 0)
|
23
|
+
|
24
|
+
# ****** BEGIN PARTITIONED PATCH ******
|
25
|
+
if self.class.respond_to?(:dynamic_arel_table)
|
26
|
+
using_arel_table = dynamic_arel_table()
|
27
|
+
relation = ActiveRecord::Relation.new(self.class, using_arel_table).
|
28
|
+
where(using_arel_table[pk].eq(substitute))
|
29
|
+
else
|
30
|
+
# ****** END PARTITIONED PATCH ******
|
31
|
+
relation = self.class.unscoped.where(
|
32
|
+
self.class.arel_table[pk].eq(substitute))
|
33
|
+
# ****** BEGIN PARTITIONED PATCH ******
|
39
34
|
end
|
40
|
-
|
41
|
-
@destroyed = true
|
42
|
-
freeze
|
43
|
-
end
|
35
|
+
# ****** END PARTITIONED PATCH ******
|
44
36
|
|
45
|
-
|
46
|
-
|
47
|
-
def update(attribute_names = @attributes.keys)
|
48
|
-
attributes_with_values = arel_attributes_values(false, false, attribute_names)
|
49
|
-
return 0 if attributes_with_values.empty?
|
50
|
-
klass = self.class
|
51
|
-
using_arel_table = self.respond_to?(:dynamic_arel_table) ? dynamic_arel_table() : klass.arel_table
|
52
|
-
stmt = klass.unscoped.where(using_arel_table[klass.primary_key].eq(id)).arel.compile_update(attributes_with_values)
|
53
|
-
klass.connection.update stmt
|
37
|
+
relation.bind_values = [[column, id]]
|
38
|
+
relation
|
54
39
|
end
|
55
40
|
|
56
|
-
#
|
57
|
-
#
|
58
|
-
#
|
59
|
-
def
|
41
|
+
# This method is patched to prefetch the primary key (if necessary) and to ensure
|
42
|
+
# that the partitioning attributes are always included (AR will exclude them
|
43
|
+
# if the db column's default value is the same as the new record's value).
|
44
|
+
def _create_record(attribute_names = @attributes.keys)
|
45
|
+
# ****** BEGIN PARTITIONED PATCH ******
|
60
46
|
if self.id.nil? && self.class.respond_to?(:prefetch_primary_key?) && self.class.prefetch_primary_key?
|
61
|
-
self.id = connection.next_sequence_value(self.class.sequence_name)
|
47
|
+
self.id = self.class.connection.next_sequence_value(self.class.sequence_name)
|
48
|
+
attribute_names |= ["id"]
|
49
|
+
end
|
50
|
+
|
51
|
+
if self.class.respond_to?(:partition_keys)
|
52
|
+
attribute_names |= self.class.partition_keys.map(&:to_s)
|
62
53
|
end
|
54
|
+
# ****** END PARTITIONED PATCH ******
|
63
55
|
|
64
|
-
attributes_values =
|
56
|
+
attributes_values = arel_attributes_with_values_for_create(attribute_names)
|
65
57
|
|
66
58
|
new_id = self.class.unscoped.insert attributes_values
|
59
|
+
self.id ||= new_id if self.class.primary_key
|
67
60
|
|
68
|
-
self.id ||= new_id
|
69
|
-
|
70
|
-
IdentityMap.add(self) if IdentityMap.enabled?
|
71
61
|
@new_record = false
|
72
62
|
id
|
73
63
|
end
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
64
|
+
|
65
|
+
# Updates the associated record with values matching those of the instance attributes.
|
66
|
+
# Returns the number of affected rows.
|
67
|
+
# NOTE(hofer): This monkeypatch intended for activerecord 4.0. Based on this code:
|
68
|
+
# https://github.com/rails/rails/blob/4-0-stable/activerecord/lib/active_record/persistence.rb#L487
|
69
|
+
def _update_record(attribute_names = @attributes.keys)
|
70
|
+
attributes_with_values = arel_attributes_with_values_for_update(attribute_names)
|
71
|
+
if attributes_with_values.empty?
|
72
|
+
0
|
73
|
+
else
|
74
|
+
klass = self.class
|
75
|
+
column_hash = klass.connection.schema_cache.columns_hash klass.table_name
|
76
|
+
db_columns_with_values = attributes_with_values.map { |attr,value|
|
77
|
+
real_column = column_hash[attr.name]
|
78
|
+
[real_column, value]
|
79
|
+
}
|
80
|
+
bind_attrs = attributes_with_values.dup
|
81
|
+
bind_attrs.keys.each_with_index do |column, i|
|
82
|
+
real_column = db_columns_with_values[i].first
|
83
|
+
bind_attrs[column] = klass.connection.substitute_at(real_column, i)
|
84
|
+
end
|
85
|
+
|
86
|
+
# ****** BEGIN PARTITIONED PATCH ******
|
87
|
+
if self.respond_to?(:dynamic_arel_table)
|
88
|
+
using_arel_table = dynamic_arel_table()
|
89
|
+
stmt = klass.unscoped.where(using_arel_table[klass.primary_key].eq(id_was || id)).arel.compile_update(bind_attrs)
|
90
|
+
|
91
|
+
# NOTE(hofer): The stmt variable got set up using
|
92
|
+
# klass.arel_table as its arel value. So arel_table.name is
|
93
|
+
# what gets used to construct the update statement. Here we
|
94
|
+
# set it to the specific partition name for this record so
|
95
|
+
# that the update gets run just on that partition, not on
|
96
|
+
# the parent one (which can cause performance issues).
|
97
|
+
begin
|
98
|
+
klass.arel_table.name = partition_table_name()
|
99
|
+
klass.connection.update stmt, 'SQL', db_columns_with_values
|
100
|
+
ensure
|
101
|
+
klass.arel_table.name = klass.table_name
|
102
|
+
end
|
103
|
+
else
|
104
|
+
# Original lines:
|
105
|
+
stmt = klass.unscoped.where(klass.arel_table[klass.primary_key].eq(id_was || id)).arel.compile_update(bind_attrs)
|
106
|
+
klass.connection.update stmt, 'SQL', db_columns_with_values
|
107
|
+
end
|
108
|
+
# ****** END PARTITIONED PATCH ******
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
end # module Persistence
|
113
|
+
|
80
114
|
module QueryMethods
|
81
115
|
|
116
|
+
# This method is patched to change the default behavior of select
|
117
|
+
# to use the Relation's Arel::Table
|
82
118
|
def build_select(arel, selects)
|
83
|
-
|
84
|
-
|
85
|
-
|
119
|
+
if !selects.empty?
|
120
|
+
expanded_select = selects.map do |field|
|
121
|
+
columns_hash.key?(field.to_s) ? arel_table[field] : field
|
122
|
+
end
|
123
|
+
arel.project(*expanded_select)
|
86
124
|
else
|
125
|
+
# ****** BEGIN PARTITIONED PATCH ******
|
126
|
+
# Original line:
|
127
|
+
# arel.project(@klass.arel_table[Arel.star])
|
87
128
|
arel.project(table[Arel.star])
|
129
|
+
# ****** END PARTITIONED PATCH ******
|
88
130
|
end
|
89
131
|
end
|
90
132
|
|
91
|
-
end
|
133
|
+
end # module QueryMethods
|
92
134
|
|
93
|
-
#
|
94
|
-
# Patches for relation to allow back hooks into the {ActiveRecord}
|
95
|
-
# requesting name of table as a function of attributes.
|
96
|
-
#
|
97
135
|
class Relation
|
98
|
-
|
99
|
-
#
|
100
|
-
#
|
101
|
-
# inserted.
|
102
|
-
#
|
103
|
-
# The differences between this and the original code are small and marked
|
104
|
-
# with PARTITIONED comment.
|
136
|
+
|
137
|
+
# This method is patched to use a table name that is derived from
|
138
|
+
# the attribute values.
|
105
139
|
def insert(values)
|
106
140
|
primary_key_value = nil
|
107
141
|
|
108
142
|
if primary_key && Hash === values
|
109
143
|
primary_key_value = values[values.keys.find { |k|
|
110
|
-
|
111
|
-
|
144
|
+
k.name == primary_key
|
145
|
+
}]
|
112
146
|
|
113
147
|
if !primary_key_value && connection.prefetch_primary_key?(klass.table_name)
|
114
148
|
primary_key_value = connection.next_sequence_value(klass.sequence_name)
|
@@ -117,15 +151,14 @@ module ActiveRecord
|
|
117
151
|
end
|
118
152
|
|
119
153
|
im = arel.create_insert
|
120
|
-
|
121
|
-
#
|
122
|
-
# current values to placed in the table (which hopefully hold the values
|
123
|
-
# that are used to determine the child table this insert should be
|
124
|
-
# redirected to)
|
125
|
-
#
|
154
|
+
|
155
|
+
# ****** BEGIN PARTITIONED PATCH ******
|
126
156
|
actual_arel_table = @klass.dynamic_arel_table(Hash[*values.map{|k,v| [k.name,v]}.flatten]) if @klass.respond_to?(:dynamic_arel_table)
|
127
157
|
actual_arel_table = @table unless actual_arel_table
|
158
|
+
# Original line:
|
159
|
+
# im.into @table
|
128
160
|
im.into actual_arel_table
|
161
|
+
# ****** END PARTITIONED PATCH ******
|
129
162
|
|
130
163
|
conn = @klass.connection
|
131
164
|
|
@@ -145,12 +178,13 @@ module ActiveRecord
|
|
145
178
|
end
|
146
179
|
|
147
180
|
conn.insert(
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
181
|
+
im,
|
182
|
+
'SQL',
|
183
|
+
primary_key,
|
184
|
+
primary_key_value,
|
185
|
+
nil,
|
186
|
+
binds)
|
154
187
|
end
|
155
|
-
|
156
|
-
end
|
188
|
+
|
189
|
+
end # class Relation
|
190
|
+
end # module ActiveRecord
|
@@ -9,19 +9,6 @@ require 'active_record/connection_adapters/postgresql_adapter'
|
|
9
9
|
# needed to abstract partition specific SQL statements.
|
10
10
|
#
|
11
11
|
module ActiveRecord::ConnectionAdapters
|
12
|
-
#
|
13
|
-
# Patches associated with building check constraints.
|
14
|
-
#
|
15
|
-
class TableDefinition
|
16
|
-
#
|
17
|
-
# Builds a SQL check constraint
|
18
|
-
#
|
19
|
-
# @param [String] constraint a SQL constraint
|
20
|
-
def check_constraint(constraint)
|
21
|
-
@columns << Struct.new(:to_sql).new("CHECK (#{constraint})")
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
12
|
#
|
26
13
|
# Patches extending the postgres adapter with new operations for managing
|
27
14
|
# sequences (and sets of sequence values), schemas and foreign keys.
|
@@ -66,7 +66,8 @@ module Partitioned
|
|
66
66
|
}
|
67
67
|
partition.check_constraint lambda { |model, time_field|
|
68
68
|
date = model.partition_normalize_key_value(time_field)
|
69
|
-
return "#{model.partition_time_field} >= '#{date.strftime}' AND
|
69
|
+
return "#{model.partition_time_field} >= '#{date.strftime('%Y-%m-%d')}' AND " +
|
70
|
+
"#{model.partition_time_field} < '#{(date + model.partition_table_size).strftime('%Y-%m-%d')}'"
|
70
71
|
}
|
71
72
|
end
|
72
73
|
end
|
@@ -150,7 +150,7 @@ module Partitioned
|
|
150
150
|
#
|
151
151
|
# Use as:
|
152
152
|
#
|
153
|
-
# Foo.from_partition(KEY).
|
153
|
+
# Foo.from_partition(KEY).first
|
154
154
|
#
|
155
155
|
# where KEY is the key value(s) used as the check constraint on Foo's table.
|
156
156
|
#
|
@@ -168,7 +168,7 @@ module Partitioned
|
|
168
168
|
#
|
169
169
|
# Use as:
|
170
170
|
#
|
171
|
-
# Foo.from_partition_without_alias(KEY).
|
171
|
+
# Foo.from_partition_without_alias(KEY).all
|
172
172
|
#
|
173
173
|
# where KEY is the key value(s) used as the check constraint on Foo's table.
|
174
174
|
#
|
@@ -38,13 +38,11 @@ module Partitioned
|
|
38
38
|
# Does a specific child partition exist.
|
39
39
|
#
|
40
40
|
def partition_exists?(*partition_key_values)
|
41
|
-
return
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
configurator.part_name(*partition_key_values)
|
47
|
-
]).count.to_i == 1
|
41
|
+
return find_by_sql([
|
42
|
+
"SELECT COUNT(*) as count FROM pg_tables WHERE schemaname = ? AND tablename = ?;",
|
43
|
+
configurator.schema_name,
|
44
|
+
configurator.part_name(*partition_key_values)
|
45
|
+
]).first.count.to_i == 1
|
48
46
|
end
|
49
47
|
|
50
48
|
#
|
@@ -57,13 +57,14 @@ module Partitioned
|
|
57
57
|
# Does a specific child partition exist.
|
58
58
|
#
|
59
59
|
def partition_exists?(*partition_key_values)
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
60
|
+
query = <<-SQL
|
61
|
+
SELECT COUNT(*) AS count
|
62
|
+
FROM pg_tables
|
63
|
+
WHERE schemaname = '#{configurator.schema_name}'
|
64
|
+
AND tablename = '#{configurator.part_name(*partition_key_values)}'
|
65
|
+
LIMIT 1
|
66
|
+
SQL
|
67
|
+
return find_by_sql(query).first.count.to_i == 1
|
67
68
|
end
|
68
69
|
|
69
70
|
#
|
@@ -90,12 +91,15 @@ module Partitioned
|
|
90
91
|
# $3 = the parameter 'how_many'
|
91
92
|
#
|
92
93
|
def last_n_partition_names(how_many = 1)
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
94
|
+
order_by_clause = last_n_partitions_order_by_clause
|
95
|
+
query = <<-SQL
|
96
|
+
SELECT tablename
|
97
|
+
FROM pg_tables
|
98
|
+
WHERE schemaname = '#{configurator.schema_name}'
|
99
|
+
#{order_by_clause.nil? ? "" : "ORDER BY " + order_by_clause}
|
100
|
+
LIMIT 1
|
101
|
+
SQL
|
102
|
+
return find_by_sql(query).map(&:tablename)
|
99
103
|
end
|
100
104
|
|
101
105
|
#
|
@@ -162,8 +166,20 @@ module Partitioned
|
|
162
166
|
:id => false,
|
163
167
|
:options => "INHERITS (#{configurator.parent_table_name(*partition_key_values)})"
|
164
168
|
}) do |t|
|
165
|
-
|
166
|
-
|
169
|
+
end
|
170
|
+
add_check_constraint(*partition_key_values)
|
171
|
+
end
|
172
|
+
|
173
|
+
#
|
174
|
+
# Add the check constraint to the table (if applicable)
|
175
|
+
#
|
176
|
+
def add_check_constraint(*partition_key_values)
|
177
|
+
constraint = configurator.check_constraint(*partition_key_values)
|
178
|
+
if constraint
|
179
|
+
sql = <<-SQL
|
180
|
+
ALTER TABLE #{configurator.table_name(*partition_key_values)} ADD CHECK (#{constraint});
|
181
|
+
SQL
|
182
|
+
execute(sql)
|
167
183
|
end
|
168
184
|
end
|
169
185
|
|
data/lib/partitioned/version.rb
CHANGED
data/partitioned.gemspec
CHANGED
@@ -7,7 +7,7 @@ Gem::Specification.new do |s|
|
|
7
7
|
s.name = 'partitioned'
|
8
8
|
s.version = Partitioned::VERSION
|
9
9
|
s.license = 'New BSD License'
|
10
|
-
s.date = '
|
10
|
+
s.date = '2015-10-01'
|
11
11
|
s.summary = "Postgres table partitioning support for ActiveRecord."
|
12
12
|
s.description = "A gem providing support for table partitioning in ActiveRecord. Support is available for postgres and AWS RedShift databases. Other features include child table management (creation and deletion) and bulk data creating and updating."
|
13
13
|
s.authors = ["Keith Gabryelski", "Aleksandr Dembskiy", "Edward Slavich"]
|
@@ -22,7 +22,7 @@ Gem::Specification.new do |s|
|
|
22
22
|
s.add_dependency 'pg'
|
23
23
|
s.add_dependency 'bulk_data_methods'
|
24
24
|
s.add_dependency 'activerecord-redshift-adapter'
|
25
|
-
s.add_dependency 'activerecord', '~>
|
26
|
-
s.add_development_dependency 'rails', '~>
|
27
|
-
s.add_development_dependency 'rspec-rails'
|
25
|
+
s.add_dependency 'activerecord', '~> 4.0.4'
|
26
|
+
s.add_development_dependency 'rails', '~> 4.0.4'
|
27
|
+
s.add_development_dependency 'rspec-rails'
|
28
28
|
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# See https://help.github.com/articles/ignoring-files for more about ignoring files.
|
2
|
+
#
|
3
|
+
# If you find yourself ignoring temporary files generated by your text editor
|
4
|
+
# or operating system, you probably want to add a global ignore instead:
|
5
|
+
# git config --global core.excludesfile '~/.gitignore_global'
|
6
|
+
|
7
|
+
# Ignore bundler config.
|
8
|
+
/.bundle
|
9
|
+
|
10
|
+
# Ignore the default SQLite database.
|
11
|
+
/db/*.sqlite3
|
12
|
+
/db/*.sqlite3-journal
|
13
|
+
|
14
|
+
# Ignore all logfiles and tempfiles.
|
15
|
+
/log/*.log
|
16
|
+
/tmp
|