partitioned 1.3.5 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -0
  3. data/.travis.yml +1 -2
  4. data/lib/monkey_patch_activerecord.rb +115 -81
  5. data/lib/monkey_patch_postgres.rb +0 -13
  6. data/lib/partitioned/by_time_field.rb +2 -1
  7. data/lib/partitioned/partitioned_base.rb +2 -2
  8. data/lib/partitioned/partitioned_base/redshift_sql_adapter.rb +5 -7
  9. data/lib/partitioned/partitioned_base/sql_adapter.rb +31 -15
  10. data/lib/partitioned/version.rb +1 -1
  11. data/partitioned.gemspec +4 -4
  12. data/spec/dummy/.gitignore +16 -0
  13. data/spec/dummy/README.rdoc +15 -248
  14. data/spec/dummy/Rakefile +0 -1
  15. data/spec/dummy/app/assets/javascripts/application.js +11 -4
  16. data/spec/dummy/app/assets/stylesheets/application.css +11 -5
  17. data/spec/dummy/app/controllers/application_controller.rb +3 -1
  18. data/spec/dummy/app/views/layouts/application.html.erb +2 -2
  19. data/spec/dummy/bin/bundle +3 -0
  20. data/spec/dummy/bin/rails +4 -0
  21. data/spec/dummy/bin/rake +4 -0
  22. data/spec/dummy/config.ru +1 -1
  23. data/spec/dummy/config/application.rb +6 -32
  24. data/spec/dummy/config/boot.rb +3 -9
  25. data/spec/dummy/config/environment.rb +2 -2
  26. data/spec/dummy/config/environments/development.rb +12 -13
  27. data/spec/dummy/config/environments/production.rb +44 -24
  28. data/spec/dummy/config/environments/test.rb +15 -18
  29. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  30. data/spec/dummy/config/initializers/inflections.rb +9 -3
  31. data/spec/dummy/config/initializers/secret_token.rb +7 -2
  32. data/spec/dummy/config/initializers/session_store.rb +1 -6
  33. data/spec/dummy/config/initializers/wrap_parameters.rb +6 -6
  34. data/spec/dummy/config/locales/en.yml +20 -2
  35. data/spec/dummy/config/routes.rb +22 -24
  36. data/spec/dummy/db/seeds.rb +7 -0
  37. data/spec/dummy/public/404.html +43 -11
  38. data/spec/dummy/public/422.html +43 -11
  39. data/spec/dummy/public/500.html +43 -12
  40. data/spec/dummy/public/robots.txt +5 -0
  41. data/spec/dummy/test/test_helper.rb +15 -0
  42. data/spec/{monkey_patch_posgres_spec.rb → monkey_patch_postgres_spec.rb} +0 -13
  43. data/spec/partitioned/by_created_at_spec.rb +1 -2
  44. data/spec/partitioned/by_daily_time_field_spec.rb +0 -1
  45. data/spec/partitioned/by_foreign_key_spec.rb +0 -1
  46. data/spec/partitioned/by_id_spec.rb +16 -3
  47. data/spec/partitioned/by_integer_field_spec.rb +0 -1
  48. data/spec/partitioned/by_monthly_time_field_spec.rb +1 -2
  49. data/spec/partitioned/by_time_field_spec.rb +1 -2
  50. data/spec/partitioned/by_weekly_time_field_spec.rb +0 -1
  51. data/spec/partitioned/by_yearly_time_field_spec.rb +1 -2
  52. data/spec/partitioned/partitioned_base/sql_adapter_spec.rb +2 -2
  53. data/spec/partitioned/partitioned_base_spec.rb +1 -1
  54. data/spec/support/shared_example_spec_helper_for_integer_key.rb +2 -2
  55. data/spec/support/shared_example_spec_helper_for_time_key.rb +1 -1
  56. data/spec/support/tables_spec_helper.rb +2 -2
  57. data/travis/before.sh +6 -0
  58. metadata +29 -16
  59. data/spec/dummy/script/rails +0 -6
  60. data/spec/dummy/spec/spec_helper.rb +0 -27
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: bb273afc943e89b665d5102b95da5edb8a08ece3
4
- data.tar.gz: 82acc9f8f6adf56e2f9819aa2a6162f83f932183
3
+ metadata.gz: be864b7e40e4e6c504bf98b195a564bde6d076b4
4
+ data.tar.gz: b79141c1dc5742385d9426f7a6fcf176844642ec
5
5
  SHA512:
6
- metadata.gz: fc7a3eb02f86a030908f2bded67c3ad0569b582b884f4226e5385f43866827fec51d671f2f06b2a1768df52fbafb0be3ce99dc8e3acc0cd95621ec3f02b0e796
7
- data.tar.gz: 65007a35629ed9f656e16d29e97e07b4d0985fb6ada1d1a2913d5380ce2e4add70f46759af5a4f8141956e857aa4542fefeeec49b5b348f69e86f6edcdd1dbf3
6
+ metadata.gz: b0a74962101fbff55ae9edc9a8203088fca7aaa078b3bbac3594a03d3d3c8bb122f545cb984c5ad5a897ffbe80c557d530180cd2b0a0af5e353ba0bb469adcdc
7
+ data.tar.gz: 06c0780a190219771c06f27091774395eb682e126bf458dcb4cc1091a41b6ece5e3d104ed343684eae41e50259c6e7fc167e7ee40b9833b0460d16e036f7d2bb
data/.gitignore CHANGED
@@ -1,3 +1,7 @@
1
1
  .ruby-version
2
2
  Gemfile.lock
3
3
  /spec/dummy/log
4
+ /pkg
5
+ /.bundle
6
+ /spec/dummy/db/structure.sql
7
+ /spec/dummy/db/schema.rb
data/.travis.yml CHANGED
@@ -9,7 +9,6 @@ rvm:
9
9
  - 2.0.0
10
10
  - 2.1.7
11
11
 
12
- before_script:
13
- - createdb part_test
12
+ before_script: travis/before.sh
14
13
 
15
14
  script: bundle exec rspec
@@ -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
- # Deletes the record in the database and freezes this instance to reflect
18
- # that no changes should be made (since they can't be persisted).
19
- def destroy
20
- destroy_associations
21
-
22
- if persisted?
23
- IdentityMap.remove(self) if IdentityMap.enabled?
24
- pk = self.class.primary_key
25
- column = self.class.columns_hash[pk]
26
- substitute = connection.substitute_at(column, 0)
27
-
28
- if self.class.respond_to?(:dynamic_arel_table)
29
- using_arel_table = dynamic_arel_table()
30
- relation = ActiveRecord::Relation.new(self.class, using_arel_table).
31
- where(using_arel_table[pk].eq(substitute))
32
- else
33
- using_arel_table = self.class.arel_table
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
- # Updates the associated record with values matching those of the instance attributes.
46
- # Returns the number of affected rows.
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
- # patch the create method to prefetch the primary key if needed
58
- #
59
- def create
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 = arel_attributes_values(!id.nil?)
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
- end
75
-
76
- #
77
- # A patch to QueryMethods to change default behavior of select
78
- # to use the Relation's Arel::Table.
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
- unless selects.empty?
84
- @implicit_readonly = false
85
- arel.project(*selects)
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
- # Patches {ActiveRecord}'s building of an insert statement to request
100
- # of the model a table name with respect to attribute values being
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
- k.name == primary_key
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
- # PARTITIONED ADDITION. get arel_table from class with respect to the
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
- im,
149
- 'SQL',
150
- primary_key,
151
- primary_key_value,
152
- nil,
153
- binds)
181
+ im,
182
+ 'SQL',
183
+ primary_key,
184
+ primary_key_value,
185
+ nil,
186
+ binds)
154
187
  end
155
- end
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 #{model.partition_time_field} < '#{(date + model.partition_table_size).strftime}'"
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).find(:first)
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).find(:all, :select => "*")
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 find(:first,
42
- :from => "pg_tables",
43
- :select => "count(*) as count",
44
- :conditions => ["schemaname = ? and tablename = ?",
45
- configurator.schema_name,
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
- return find(:first,
61
- :from => "pg_tables",
62
- :select => "count(*) as count",
63
- :conditions => ["schemaname = ? and tablename = ?",
64
- configurator.schema_name,
65
- configurator.part_name(*partition_key_values)
66
- ]).count.to_i == 1
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
- return find(:all,
94
- :from => "pg_tables",
95
- :select => :tablename,
96
- :conditions => ["schemaname = ?", configurator.schema_name],
97
- :order => last_n_partitions_order_by_clause,
98
- :limit => how_many).map(&:tablename)
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
- constraint = configurator.check_constraint(*partition_key_values)
166
- t.check_constraint constraint if constraint
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
 
@@ -1,4 +1,4 @@
1
1
  module Partitioned
2
2
  # the current version of this gem
3
- VERSION = "1.3.5"
3
+ VERSION = "2.0.0"
4
4
  end
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 = '2013-12-13'
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', '~> 3.0'
26
- s.add_development_dependency 'rails', '~> 3.2.8'
27
- s.add_development_dependency 'rspec-rails', '~> 3.0'
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