aspgems-foreign_key_migrations 2.0.0.beta1

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 (35) hide show
  1. data/.gitignore +3 -0
  2. data/CHANGELOG +109 -0
  3. data/Gemfile +3 -0
  4. data/MIT-LICENSE +20 -0
  5. data/README.md +91 -0
  6. data/Rakefile +69 -0
  7. data/foreign_key_migrations.gemspec +28 -0
  8. data/init.rb +1 -0
  9. data/install.rb +1 -0
  10. data/lib/foreign_key_migrations.rb +48 -0
  11. data/lib/foreign_key_migrations/active_record/base.rb +36 -0
  12. data/lib/foreign_key_migrations/active_record/connection_adapters/schema_statements.rb +23 -0
  13. data/lib/foreign_key_migrations/active_record/connection_adapters/table_definition.rb +50 -0
  14. data/lib/foreign_key_migrations/active_record/migration.rb +62 -0
  15. data/lib/foreign_key_migrations/version.rb +3 -0
  16. data/lib/generators/foreign_key_migrations/migration_generator.rb +38 -0
  17. data/lib/generators/foreign_key_migrations/templates/migration.rb +10 -0
  18. data/spec/aaa_create_tables_spec.rb +9 -0
  19. data/spec/connections/mysql/connection.rb +16 -0
  20. data/spec/connections/mysql2/connection.rb +16 -0
  21. data/spec/connections/postgresql/connection.rb +13 -0
  22. data/spec/migration_spec.rb +257 -0
  23. data/spec/models/comment.rb +3 -0
  24. data/spec/models/post.rb +3 -0
  25. data/spec/models/user.rb +2 -0
  26. data/spec/references_spec.rb +48 -0
  27. data/spec/schema/schema.rb +24 -0
  28. data/spec/schema_dumper_spec.rb +32 -0
  29. data/spec/schema_spec.rb +65 -0
  30. data/spec/spec_helper.rb +15 -0
  31. data/spec/support/helpers.rb +8 -0
  32. data/spec/support/matchers/foreign_key_migrations_matchers.rb +2 -0
  33. data/spec/support/matchers/have_index.rb +52 -0
  34. data/spec/support/matchers/reference.rb +66 -0
  35. metadata +144 -0
@@ -0,0 +1,50 @@
1
+ module ForeignKeyMigrations::ActiveRecord::ConnectionAdapters
2
+ module TableDefinition
3
+ def self.included(base)
4
+ base.class_eval do
5
+ alias_method_chain :column, :foreign_key_migrations
6
+ alias_method_chain :primary_key, :foreign_key_migrations
7
+ end
8
+ end
9
+
10
+ def primary_key_with_foreign_key_migrations(name, options = {})
11
+ column(name, :primary_key, options)
12
+ end
13
+
14
+ def indices
15
+ @indices ||= []
16
+ end
17
+
18
+ def column_with_foreign_key_migrations(name, type, options = {})
19
+ column_without_foreign_key_migrations(name, type, options)
20
+ references = ActiveRecord::Base.references(self.name, name, options)
21
+ if references
22
+ ForeignKeyMigrations.set_default_update_and_delete_actions!(options)
23
+ foreign_key(name, references.first, references.last, options)
24
+ if index = fkm_index_options(options)
25
+ # append [column_name, index_options] pair
26
+ self.indices << [name, ForeignKeyMigrations.options_for_index(index)]
27
+ end
28
+ elsif options[:index]
29
+ self.indices << [name, ForeignKeyMigrations.options_for_index(options[:index])]
30
+ end
31
+ self
32
+ end
33
+
34
+ # Some people liked this; personally I've decided against using it but I'll keep it nonetheless
35
+ def belongs_to(table, options = {})
36
+ options = options.merge(:references => table)
37
+ options[:on_delete] = options.delete(:dependent) if options.has_key?(:dependent)
38
+ column("#{table.to_s.singularize}_id".to_sym, :integer, options)
39
+ end
40
+
41
+ protected
42
+ def fkm_index_options(options)
43
+ options.fetch(:index, fkm_use_auto_index?)
44
+ end
45
+
46
+ def fkm_use_auto_index?
47
+ ForeignKeyMigrations.auto_index && !ActiveRecord::Schema.defining?
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,62 @@
1
+ module ForeignKeyMigrations::ActiveRecord
2
+ module Migration
3
+ def self.included(base) # :nodoc
4
+ base.extend(ClassMethods)
5
+ end
6
+
7
+ module ClassMethods
8
+ # Overrides ActiveRecord#add_column and adds foreign key if column references other column
9
+ #
10
+ # add_column('comments', 'post_id', :integer)
11
+ # # creates a column and adds foreign key on posts(id)
12
+ #
13
+ # add_column('comments', 'post_id', :integer, :on_update => :cascade, :on_delete => :cascade)
14
+ # # creates a column and adds foreign key on posts(id) with cascade actions on update and on delete
15
+ #
16
+ # add_column('comments', 'post_id', :integer, :index => true)
17
+ # # creates a column and adds foreign key on posts(id)
18
+ # # additionally adds index on posts(id)
19
+ #
20
+ # add_column('comments', 'post_id', :integer, :index => { :unique => true, :name => 'comments_post_id_unique_index' }))
21
+ # # creates a column and adds foreign key on posts(id)
22
+ # # additionally adds unique index on posts(id) named comments_post_id_unique_index
23
+ #
24
+ # add_column('addresses', 'citizen_id', :integer, :references => :users
25
+ # # creates a column and adds foreign key on users(id)
26
+ #
27
+ # add_column('addresses', 'citizen_id', :integer, :references => [:users, :uuid]
28
+ # # creates a column and adds foreign key on users(uuid)
29
+ #
30
+ def add_column(table_name, column_name, type, options = {})
31
+ super
32
+ handle_column_options(table_name, column_name, options)
33
+ end
34
+
35
+ def change_column(table_name, column_name, type, options = {})
36
+ super
37
+ remove_foreign_key_if_exists(table_name, column_name)
38
+ handle_column_options(table_name, column_name, options)
39
+ end
40
+
41
+ protected
42
+ def handle_column_options(table_name, column_name, options)
43
+ references = ActiveRecord::Base.references(table_name, column_name, options)
44
+ if references
45
+ ForeignKeyMigrations.set_default_update_and_delete_actions!(options)
46
+ add_foreign_key(table_name, column_name, references.first, references.last, options)
47
+ if index = options.fetch(:index, ForeignKeyMigrations.auto_index)
48
+ add_index(table_name, column_name, ForeignKeyMigrations.options_for_index(index))
49
+ end
50
+ elsif options[:index]
51
+ add_index(table_name, column_name, ForeignKeyMigrations.options_for_index(options[:index]))
52
+ end
53
+ end
54
+
55
+ def remove_foreign_key_if_exists(table_name, column_name)
56
+ foreign_keys = ActiveRecord::Base.connection.foreign_keys(table_name.to_s)
57
+ fk = foreign_keys.detect { |fk| fk.table_name == table_name.to_s && fk.column_names == Array(column_name).collect(&:to_s) }
58
+ remove_foreign_key(table_name, fk.name) if fk
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,3 @@
1
+ module ForeignKeyMigrations
2
+ VERSION = "2.0.0.beta1"
3
+ end
@@ -0,0 +1,38 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/active_record'
3
+
4
+ module ForeignKeyMigrations
5
+ module Generators
6
+ class MigrationGenerator < ::ActiveRecord::Generators::Base
7
+ argument :name, :default => 'create_foreign_keys'
8
+
9
+ def self.source_root
10
+ File.expand_path(File.join(File.dirname(__FILE__), 'templates'))
11
+ end
12
+
13
+ def create_migration_file
14
+ set_local_assigns!
15
+ migration_template 'migration.rb', "db/migrate/#{file_name}"
16
+ end
17
+
18
+ protected
19
+ attr_reader :foreign_keys
20
+
21
+ def set_local_assigns!
22
+ @foreign_keys = determine_foreign_keys
23
+ end
24
+
25
+ def determine_foreign_keys
26
+ foreign_keys = []
27
+ connection = ::ActiveRecord::Base.connection
28
+ connection.tables.each do |table_name|
29
+ connection.columns(table_name).each do |column|
30
+ references = ::ActiveRecord::Base.references(table_name, column.name)
31
+ foreign_keys << ::RedhillonrailsCore::ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(nil, table_name, column.name, references.first, references.last) if references
32
+ end
33
+ end
34
+ foreign_keys
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,10 @@
1
+ class <%= migration_name %> < ActiveRecord::Migration
2
+ def self.up
3
+ <% foreign_keys.each do |foreign_key| -%>
4
+ <%= foreign_key.to_dump %>
5
+ <% end -%>
6
+ end
7
+
8
+ def self.down
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ # encoding: utf-8
2
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
3
+
4
+ # This is not a test really. It just here to create schema
5
+ # The filename begins with "aaa" to ensure this is executed initially
6
+
7
+ ActiveRecord::Migration.suppress_messages do
8
+ load_schema
9
+ end
@@ -0,0 +1,16 @@
1
+ print "Using MySQL\n"
2
+ require 'logger'
3
+
4
+ ActiveRecord::Base.configurations = {
5
+ 'fkm' => {
6
+ :adapter => 'mysql',
7
+ :database => 'fkm_unittest',
8
+ :username => 'fkm',
9
+ :encoding => 'utf8',
10
+ :socket => '/var/run/mysqld/mysqld.sock',
11
+ :min_messages => 'warning'
12
+ }
13
+
14
+ }
15
+
16
+ ActiveRecord::Base.establish_connection 'fkm'
@@ -0,0 +1,16 @@
1
+ print "Using MySQL2\n"
2
+ require 'logger'
3
+
4
+ ActiveRecord::Base.configurations = {
5
+ 'fkm' => {
6
+ :adapter => 'mysql2',
7
+ :database => 'fkm_unittest',
8
+ :username => 'fkm',
9
+ :encoding => 'utf8',
10
+ :socket => '/var/run/mysqld/mysqld.sock',
11
+ :min_messages => 'warning'
12
+ }
13
+
14
+ }
15
+
16
+ ActiveRecord::Base.establish_connection 'fkm'
@@ -0,0 +1,13 @@
1
+ print "Using PostgreSQL\n"
2
+ require 'logger'
3
+
4
+ ActiveRecord::Base.configurations = {
5
+ 'fkm' => {
6
+ :adapter => 'postgresql',
7
+ :database => 'fkm_unittest',
8
+ :min_messages => 'warning'
9
+ }
10
+
11
+ }
12
+
13
+ ActiveRecord::Base.establish_connection 'fkm'
@@ -0,0 +1,257 @@
1
+ # encoding: utf-8
2
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
3
+
4
+ require 'models/post'
5
+ require 'models/comment'
6
+
7
+ describe ActiveRecord::Base do
8
+
9
+ it "should respond to references" do
10
+ ActiveRecord::Base.should respond_to :references
11
+ end
12
+
13
+ end
14
+
15
+ describe ActiveRecord::Migration do
16
+ include ForeignKeyMigrationsHelpers
17
+
18
+ context "when table is created" do
19
+
20
+ before(:each) do
21
+ @model = Post
22
+ end
23
+
24
+ it "should create foreign keys" do
25
+ create_table(@model, :user_id => {},
26
+ :author_id => { :references => :users },
27
+ :member_id => { :references => nil } )
28
+ @model.should reference(:users, :id).on(:user_id)
29
+ @model.should reference(:users, :id).on(:author_id)
30
+ @model.should_not reference.on(:member_id)
31
+ end
32
+
33
+ it "should use default on_cascade action" do
34
+ ForeignKeyMigrations.on_update = :cascade
35
+ create_table(@model, :user_id => {})
36
+ ForeignKeyMigrations.on_update = nil
37
+ @model.should reference.on(:user_id).on_update(:cascade)
38
+ end
39
+
40
+ it "should use default on_cascade action" do
41
+ ForeignKeyMigrations.on_delete = :cascade
42
+ create_table(@model, :user_id => {})
43
+ ForeignKeyMigrations.on_delete = nil
44
+ @model.should reference.on(:user_id).on_delete(:cascade)
45
+ end
46
+
47
+ it "should create an index if specified" do
48
+ create_table(@model, :state => { :index => true })
49
+ @model.should have_index.on(:state)
50
+ end
51
+
52
+ it "should create a multiple-column index if specified" do
53
+ create_table(@model, :city => {},
54
+ :state => { :index => {:with => :city} } )
55
+ @model.should have_index.on([:state, :city])
56
+ end
57
+
58
+ it "should auto-index foreign keys only" do
59
+ ForeignKeyMigrations.auto_index = true
60
+ create_table(@model, :user_id => {},
61
+ :application_id => { :references => nil },
62
+ :state => {})
63
+ @model.should have_index.on(:user_id)
64
+ @model.should_not have_index.on(:application_id)
65
+ @model.should_not have_index.on(:state)
66
+ ForeignKeyMigrations.auto_index = nil
67
+ end
68
+
69
+ end
70
+
71
+ context "when column is added" do
72
+
73
+ before(:each) do
74
+ @model = Comment
75
+ end
76
+
77
+ it "should create foreign key" do
78
+ add_column(:post_id, :integer) do
79
+ @model.should reference(:posts, :id).on(:post_id)
80
+ end
81
+ end
82
+
83
+ it "should create foreign key to explicity given table" do
84
+ add_column(:author_id, :integer, :references => :users) do
85
+ @model.should reference(:users, :id).on(:author_id)
86
+ end
87
+ end
88
+
89
+ it "should create foreign key to explicity given table and column name" do
90
+ add_column(:author_login, :string, :references => [:users, :login]) do
91
+ @model.should reference(:users, :login).on(:author_login)
92
+ end
93
+ end
94
+
95
+ it "should create foreign key to the same table on parent_id" do
96
+ add_column(:parent_id, :integer) do
97
+ @model.should reference(@model.table_name, :id).on(:parent_id)
98
+ end
99
+ end
100
+
101
+ it "shouldn't create foreign key if column doesn't look like foreign key" do
102
+ add_column(:views_count, :integer) do
103
+ @model.should_not reference.on(:views_count)
104
+ end
105
+ end
106
+
107
+ it "shouldnt't create foreign key if specified explicity" do
108
+ add_column(:post_id, :integer, :references => nil) do
109
+ @model.should_not reference.on(:post_id)
110
+ end
111
+ end
112
+
113
+ it "should create an index if specified" do
114
+ add_column(:post_id, :integer, :index => true) do
115
+ @model.should have_index.on(:post_id)
116
+ end
117
+ end
118
+
119
+ it "should create a unique index if specified" do
120
+ add_column(:post_id, :integer, :index => { :unique => true }) do
121
+ @model.should have_unique_index.on(:post_id)
122
+ end
123
+ end
124
+
125
+ it "should allow custom name for index" do
126
+ index_name = 'comments_post_id_unique_index'
127
+ add_column(:post_id, :integer, :index => { :unique => true, :name => index_name }) do
128
+ @model.should have_unique_index(:name => index_name).on(:post_id)
129
+ end
130
+ end
131
+
132
+ it "should auto-index if specified in global options" do
133
+ ForeignKeyMigrations.auto_index = true
134
+ add_column(:post_id, :integer) do
135
+ @model.should have_index.on(:post_id)
136
+ end
137
+ ForeignKeyMigrations.auto_index = false
138
+ end
139
+
140
+ it "should auto-index foreign keys only" do
141
+ ForeignKeyMigrations.auto_index = true
142
+ add_column(:state, :integer) do
143
+ @model.should_not have_index.on(:state)
144
+ end
145
+ ForeignKeyMigrations.auto_index = false
146
+ end
147
+
148
+ it "should allow to overwrite auto_index options in column definition" do
149
+ ForeignKeyMigrations.auto_index = true
150
+ add_column(:post_id, :integer, :index => false) do
151
+ # MySQL creates an index on foreign by default
152
+ # and we can do nothing with that
153
+ unless mysql?
154
+ @model.should_not have_index.on(:post_id)
155
+ end
156
+ end
157
+ ForeignKeyMigrations.auto_index = false
158
+ end
159
+
160
+ it "should use default on_update action" do
161
+ ForeignKeyMigrations.on_update = :cascade
162
+ add_column(:post_id, :integer) do
163
+ @model.should reference.on(:post_id).on_update(:cascade)
164
+ end
165
+ ForeignKeyMigrations.on_update = nil
166
+ end
167
+
168
+ it "should use default on_delete action" do
169
+ ForeignKeyMigrations.on_delete = :cascade
170
+ add_column(:post_id, :integer) do
171
+ @model.should reference.on(:post_id).on_delete(:cascade)
172
+ end
173
+ ForeignKeyMigrations.on_delete = nil
174
+ end
175
+
176
+ it "should allow to overwrite default actions" do
177
+ ForeignKeyMigrations.on_delete = :cascade
178
+ ForeignKeyMigrations.on_update = :restrict
179
+ add_column(:post_id, :integer, :on_update => :set_null, :on_delete => :set_null) do
180
+ @model.should reference.on(:post_id).on_delete(:set_null).on_update(:set_null)
181
+ end
182
+ ForeignKeyMigrations.on_delete = nil
183
+ end
184
+
185
+ protected
186
+ def add_column(column_name, *args)
187
+ table = @model.table_name
188
+ ActiveRecord::Migration.suppress_messages do
189
+ ActiveRecord::Migration.add_column(table, column_name, *args)
190
+ @model.reset_column_information
191
+ yield if block_given?
192
+ ActiveRecord::Migration.remove_column(table, column_name)
193
+ end
194
+ end
195
+
196
+ end
197
+
198
+ context "when column is changed" do
199
+
200
+ before(:each) do
201
+ @model = Comment
202
+ end
203
+
204
+ it "should create foreign key" do
205
+ change_column :user, :string, :references => [:users, :login]
206
+ @model.should reference(:users, :login).on(:user)
207
+ change_column :user, :string, :references => nil
208
+ end
209
+
210
+ context "and initially references to users table" do
211
+
212
+ it "should have foreign key" do
213
+ @model.should reference(:users)
214
+ end
215
+
216
+ it "should drop foreign key afterwards" do
217
+ change_column :user_id, :integer, :references => :members
218
+ @model.should_not reference(:users)
219
+ change_column :user_id, :integer, :references => :users
220
+ end
221
+
222
+ it "should reference pointed table afterwards" do
223
+ change_column :user_id, :integer, :references => :members
224
+ @model.should reference(:members)
225
+ end
226
+
227
+ end
228
+
229
+ protected
230
+ def change_column(column_name, *args)
231
+ table = @model.table_name
232
+ ActiveRecord::Migration.suppress_messages do
233
+ ActiveRecord::Migration.change_column(table, column_name, *args)
234
+ @model.reset_column_information
235
+ end
236
+ end
237
+
238
+ end
239
+
240
+ def foreign_key(model, column)
241
+ columns = Array(column).collect(&:to_s)
242
+ model.foreign_keys.detect { |fk| fk.table_name == model.table_name && fk.column_names == columns }
243
+ end
244
+
245
+ def create_table(model, columns_with_options)
246
+ ActiveRecord::Migration.suppress_messages do
247
+ ActiveRecord::Migration.create_table model.table_name, :force => true do |t|
248
+ columns_with_options.each_pair do |column, options|
249
+ t.integer column, options
250
+ end
251
+ end
252
+ model.reset_column_information
253
+ end
254
+ end
255
+
256
+ end
257
+