schema_plus 0.1.2 → 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +0 -1
- data/README.rdoc +11 -8
- data/gemfiles/Gemfile.rails-2.3.lock +65 -0
- data/gemfiles/Gemfile.rails-3.0.lock +111 -0
- data/gemfiles/Gemfile.rails-3.1 +1 -1
- data/gemfiles/Gemfile.rails-3.1.lock +123 -0
- data/lib/schema_plus.rb +1 -2
- data/lib/schema_plus/active_record/base.rb +3 -10
- data/lib/schema_plus/active_record/connection_adapters/abstract_adapter.rb +11 -1
- data/lib/schema_plus/active_record/connection_adapters/column.rb +3 -2
- data/lib/schema_plus/active_record/connection_adapters/table_definition.rb +1 -1
- data/lib/schema_plus/active_record/foreign_keys.rb +177 -0
- data/lib/schema_plus/version.rb +1 -1
- data/runspecs +35 -16
- data/spec/migration_spec.rb +10 -8
- data/spec/rails3_migration_spec.rb +138 -0
- data/spec/references_spec.rb +1 -1
- data/spec/support/helpers.rb +4 -0
- metadata +102 -129
- data/lib/schema_plus/active_record/migration.rb +0 -220
@@ -86,7 +86,7 @@ module SchemaPlus::ActiveRecord::ConnectionAdapters
|
|
86
86
|
|
87
87
|
def column_with_schema_plus(name, type, options = {}) #:nodoc:
|
88
88
|
column_without_schema_plus(name, type, options)
|
89
|
-
if references = ActiveRecord::Migration.get_references(self.name, name, options, schema_plus_config)
|
89
|
+
if references = ActiveRecord::Migration.connection.get_references(self.name, name, options, schema_plus_config)
|
90
90
|
if index = options.fetch(:index, fk_use_auto_index?)
|
91
91
|
self.column_index(name, index)
|
92
92
|
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
module SchemaPlus::ActiveRecord
|
2
|
+
module ForeignKeys
|
3
|
+
# Enhances ActiveRecord::ConnectionAdapters::AbstractAdapter#add_column to support indexes and foreign keys, with automatic creation
|
4
|
+
#
|
5
|
+
# == Indexes
|
6
|
+
#
|
7
|
+
# The <tt>:index</tt> option takes a hash of parameters to pass to ActiveRecord::Migration.add_index. Thus
|
8
|
+
#
|
9
|
+
# add_column('books', 'isbn', :string, :index => {:name => "ISBN-index", :unique => true })
|
10
|
+
#
|
11
|
+
# is equivalent to:
|
12
|
+
#
|
13
|
+
# add_column('books', 'isbn', :string)
|
14
|
+
# add_index('books', ['isbn'], :name => "ISBN-index", :unique => true)
|
15
|
+
#
|
16
|
+
#
|
17
|
+
# In order to support multi-column indexes, an special parameter <tt>:with</tt> may be specified, which takes another column name or an array of column names to include in the index. Thus
|
18
|
+
#
|
19
|
+
# add_column('contacts', 'phone_number', :string, :index => { :with => [:country_code, :area_code], :unique => true })
|
20
|
+
#
|
21
|
+
# is equivalent to:
|
22
|
+
#
|
23
|
+
# add_column('contacts', 'phone_number', :string)
|
24
|
+
# add_index('contacts', ['phone_number', 'country_code', 'area_code'], :unique => true)
|
25
|
+
#
|
26
|
+
#
|
27
|
+
# Some convenient shorthands are available:
|
28
|
+
#
|
29
|
+
# add_column('books', 'isbn', :index => true) # adds index with no extra options
|
30
|
+
# add_column('books', 'isbn', :index => :unique) # adds index with :unique => true
|
31
|
+
#
|
32
|
+
# == Foreign Key Constraints
|
33
|
+
#
|
34
|
+
# The +:references+ option takes the name of a table to reference in
|
35
|
+
# a foreign key constraint. For example:
|
36
|
+
#
|
37
|
+
# add_column('widgets', 'color', :integer, :references => 'colors')
|
38
|
+
#
|
39
|
+
# is equivalent to
|
40
|
+
#
|
41
|
+
# add_column('widgets', 'color', :integer)
|
42
|
+
# add_foreign_key('widgets', 'color', 'colors', 'id')
|
43
|
+
#
|
44
|
+
# The foreign column name defaults to +id+, but a different column
|
45
|
+
# can be specified using <tt>:references => [table_name,column_name]</tt>
|
46
|
+
#
|
47
|
+
# Additional options +:on_update+ and +:on_delete+ can be spcified,
|
48
|
+
# with values as described at ConnectionAdapters::ForeignKeyDefinition. For example:
|
49
|
+
#
|
50
|
+
# add_column('comments', 'post', :integer, :references => 'posts', :on_delete => :cascade)
|
51
|
+
#
|
52
|
+
# Global default values for +:on_update+ and +:on_delete+ can be
|
53
|
+
# specified in SchemaPlus.steup via, e.g., <tt>config.foreign_keys.on_update = :cascade</tt>
|
54
|
+
#
|
55
|
+
# == Automatic Foreign Key Constraints
|
56
|
+
#
|
57
|
+
# SchemaPlus supports the convention of naming foreign key columns
|
58
|
+
# with a suffix of +_id+. That is, if you define a column suffixed
|
59
|
+
# with +_id+, SchemaPlus assumes an implied :references to a table
|
60
|
+
# whose name is the column name prefix, pluralized. For example,
|
61
|
+
# these are equivalent:
|
62
|
+
#
|
63
|
+
# add_column('posts', 'author_id', :integer)
|
64
|
+
# add_column('posts', 'author_id', :integer, :references => 'authors')
|
65
|
+
#
|
66
|
+
# As a special case, if the column is named 'parent_id', SchemaPlus
|
67
|
+
# assumes it's a self reference, for a record that acts as a node of
|
68
|
+
# a tree. Thus, these are equivalent:
|
69
|
+
#
|
70
|
+
# add_column('sections', 'parent_id', :integer)
|
71
|
+
# add_column('sections', 'parent_id', :integer, :references => 'sections')
|
72
|
+
#
|
73
|
+
# If the implicit +:references+ value isn't what you want (e.g., the
|
74
|
+
# table name isn't pluralized), you can explicitly specify
|
75
|
+
# +:references+ and it will override the implicit value.
|
76
|
+
#
|
77
|
+
# If you don't want a foreign key constraint to be created, specify
|
78
|
+
# <tt>:references => nil</tt>.
|
79
|
+
# To disable automatic foreign key constraint creation globally, set
|
80
|
+
# <tt>config.foreign_keys.auto_create = false</tt> in
|
81
|
+
# SchemaPlus.steup.
|
82
|
+
#
|
83
|
+
# == Automatic Foreign Key Indexes
|
84
|
+
#
|
85
|
+
# Since efficient use of foreign key constraints requires that the
|
86
|
+
# referencing column be indexed, SchemaPlus will automatically create
|
87
|
+
# an index for the column if it created a foreign key. Thus
|
88
|
+
#
|
89
|
+
# add_column('widgets', 'color', :integer, :references => 'colors')
|
90
|
+
#
|
91
|
+
# is equivalent to:
|
92
|
+
#
|
93
|
+
# add_column('widgets', 'color', :integer, :references => 'colors', :index => true)
|
94
|
+
#
|
95
|
+
# If you want to pass options to the index, you can explcitly pass
|
96
|
+
# index options, such as <tt>:index => :unique</tt>.
|
97
|
+
#
|
98
|
+
# If you don't want an index to be created, specify
|
99
|
+
# <tt>:index => nil</tt>.
|
100
|
+
# To disable automatic foreign key index creation globally, set
|
101
|
+
# <tt>config.foreign_keys.auto_index = false</tt> in
|
102
|
+
# SchemaPlus.steup. (*Note*: If you're using MySQL, it will
|
103
|
+
# automatically create an index for foreign keys if you don't.)
|
104
|
+
#
|
105
|
+
def add_column(table_name, column_name, type, options = {})
|
106
|
+
super
|
107
|
+
handle_column_options(table_name, column_name, options)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Enhances ActiveRecord::Migration#change_column to support indexes and foreign keys same as add_column.
|
111
|
+
def change_column(table_name, column_name, type, options = {})
|
112
|
+
super
|
113
|
+
remove_foreign_key_if_exists(table_name, column_name)
|
114
|
+
handle_column_options(table_name, column_name, options)
|
115
|
+
end
|
116
|
+
|
117
|
+
# Determines referenced table and column.
|
118
|
+
# Used in migrations.
|
119
|
+
#
|
120
|
+
# If auto_create is true:
|
121
|
+
# get_references('comments', 'post_id') # => ['posts', 'id']
|
122
|
+
#
|
123
|
+
# And if <tt>column_name</tt> is parent_id it references to the same table
|
124
|
+
# get_references('pages', 'parent_id') # => ['pages', 'id']
|
125
|
+
#
|
126
|
+
# If :references option is given, it is used (whether or not auto_create is true)
|
127
|
+
# get_references('widgets', 'main_page_id', :references => 'pages'))
|
128
|
+
# # => ['pages', 'id']
|
129
|
+
#
|
130
|
+
# Also the referenced id column may be specified:
|
131
|
+
# get_references('addresses', 'member_id', :references => ['users', 'uuid'])
|
132
|
+
# # => ['users', 'uuid']
|
133
|
+
def get_references(table_name, column_name, options = {}, config=nil) #:nodoc:
|
134
|
+
column_name = column_name.to_s
|
135
|
+
if options.has_key?(:references)
|
136
|
+
references = options[:references]
|
137
|
+
references = [references, :id] unless references.nil? || references.is_a?(Array)
|
138
|
+
references
|
139
|
+
elsif (config || SchemaPlus.config).foreign_keys.auto_create? && !ActiveRecord::Schema.defining?
|
140
|
+
if column_name == 'parent_id'
|
141
|
+
[table_name, :id]
|
142
|
+
elsif column_name =~ /^(.*)_id$/
|
143
|
+
determined_table_name = ActiveRecord::Base.pluralize_table_names ? $1.to_s.pluralize : $1
|
144
|
+
[determined_table_name, :id]
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
protected
|
150
|
+
def handle_column_options(table_name, column_name, options) #:nodoc:
|
151
|
+
if references = get_references(table_name, column_name, options)
|
152
|
+
if index = options.fetch(:index, SchemaPlus.config.foreign_keys.auto_index? && !ActiveRecord::Schema.defining?)
|
153
|
+
column_index(table_name, column_name, index)
|
154
|
+
end
|
155
|
+
add_foreign_key(table_name, column_name, references.first, references.last,
|
156
|
+
options.reverse_merge(:on_update => SchemaPlus.config.foreign_keys.on_update,
|
157
|
+
:on_delete => SchemaPlus.config.foreign_keys.on_delete))
|
158
|
+
elsif options[:index]
|
159
|
+
column_index(table_name, column_name, options[:index])
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def column_index(table_name, column_name, options) #:nodoc:
|
164
|
+
options = {} if options == true
|
165
|
+
options = { :unique => true } if options == :unique
|
166
|
+
column_name = [column_name] + Array.wrap(options.delete(:with)).compact
|
167
|
+
add_index(table_name, column_name, options)
|
168
|
+
end
|
169
|
+
|
170
|
+
def remove_foreign_key_if_exists(table_name, column_name) #:nodoc:
|
171
|
+
foreign_keys = ActiveRecord::Base.connection.foreign_keys(table_name.to_s)
|
172
|
+
fk = foreign_keys.detect { |fk| fk.table_name == table_name.to_s && fk.column_names == Array(column_name).collect(&:to_s) }
|
173
|
+
remove_foreign_key(table_name, fk.name) if fk
|
174
|
+
end
|
175
|
+
|
176
|
+
end
|
177
|
+
end
|
data/lib/schema_plus/version.rb
CHANGED
data/runspecs
CHANGED
@@ -3,26 +3,32 @@
|
|
3
3
|
require 'optparse'
|
4
4
|
require 'ostruct'
|
5
5
|
|
6
|
-
PROJECT = File.basename(File.expand_path('..', __FILE__))
|
7
|
-
|
8
6
|
RUBY_VERSIONS = %W[1.8.7 1.9.2]
|
9
7
|
RAILS_VERSIONS = %W[2.3 3.0 3.1]
|
8
|
+
DB_ADAPTERS = %W[postgresql mysql mysql2 sqlite3]
|
10
9
|
|
11
10
|
o = OpenStruct.new
|
12
|
-
o.ruby_versions =
|
11
|
+
o.ruby_versions = ["1.9.2"]
|
13
12
|
o.rails_versions = RAILS_VERSIONS
|
14
|
-
o.
|
13
|
+
o.db_adapters = DB_ADAPTERS - ["mysql"]
|
15
14
|
|
16
15
|
OptionParser.new do |opts|
|
17
16
|
opts.banner = "Usage: #{$0} [options]"
|
18
17
|
|
18
|
+
opts.on("-n", "--dry-run", "Do a dry run without executing actions") do |v|
|
19
|
+
o.dry_run = true
|
20
|
+
end
|
21
|
+
|
22
|
+
opts.on("--update", "Update gem dependencies") do |v|
|
23
|
+
o.update = v
|
24
|
+
end
|
25
|
+
|
19
26
|
opts.on("--install", "Install gem dependencies") do |v|
|
20
27
|
o.install = v
|
21
28
|
end
|
22
29
|
|
23
|
-
opts.on("--db adapter", String, "Choose which db adapter to run
|
24
|
-
|
25
|
-
o.run_cmd = "rake #{adapter}:spec"
|
30
|
+
opts.on("--db adapter", String, "Choose which db adapter(s) to run. Default is: #{o.db_adapters.join(' ')}" ) do |adapter|
|
31
|
+
o.db_adapters = adapter.split
|
26
32
|
end
|
27
33
|
|
28
34
|
opts.on("--ruby version", String, "Choose which version(s) of ruby to run. Default is: #{o.ruby_versions.join(' ')}") do |ruby|
|
@@ -33,29 +39,42 @@ OptionParser.new do |opts|
|
|
33
39
|
o.rails_versions = rails.split(' ')
|
34
40
|
end
|
35
41
|
|
42
|
+
opts.on("--full", "run complete matrix of ruby, rails, and db") do
|
43
|
+
o.ruby_versions = RUBY_VERSIONS
|
44
|
+
o.rails_versions = RAILS_VERSIONS
|
45
|
+
o.db_adapters = DB_ADAPTERS
|
46
|
+
end
|
47
|
+
|
36
48
|
opts.on("--quick", "quick run on Postgres, ruby #{RUBY_VERSIONS.last} and rails #{RAILS_VERSIONS.last}") do
|
37
49
|
o.ruby_versions = [RUBY_VERSIONS.last]
|
38
50
|
o.rails_versions = [RAILS_VERSIONS.last]
|
39
|
-
o.
|
51
|
+
o.db_adapters = ["postgresql"]
|
40
52
|
end
|
41
53
|
|
42
54
|
end.parse!
|
43
55
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
56
|
+
cmd = case
|
57
|
+
when o.update
|
58
|
+
"bundle update"
|
59
|
+
when o.install
|
60
|
+
"bundle install"
|
61
|
+
else
|
62
|
+
"bundle exec rake #{o.db_adapters.join(":spec ")}:spec"
|
63
|
+
end
|
49
64
|
|
50
65
|
n = 1
|
51
66
|
GEMFILES_DIR = File.expand_path('../gemfiles', __FILE__)
|
52
67
|
total = o.ruby_versions.size * o.rails_versions.size
|
68
|
+
errs = []
|
53
69
|
o.ruby_versions.each do |ruby|
|
54
70
|
o.rails_versions.each do |rails|
|
55
|
-
puts "\n\n*** ruby version #{ruby} - rails version #{rails} [#{n} of #{total}]\n\n"
|
71
|
+
puts "\n\n*** ruby version #{ruby} - rails version #{rails} - db adapters: #{o.db_adapters.join(' ')} [#{n} of #{total}]\n\n"
|
56
72
|
gemfile = File.join(GEMFILES_DIR, "Gemfile.rails-#{rails}")
|
57
73
|
n += 1
|
58
|
-
command = %Q{BUNDLE_GEMFILE="#{gemfile}" rvm #{ruby} exec #{
|
59
|
-
|
74
|
+
command = %Q{BUNDLE_GEMFILE="#{gemfile}" rvm #{ruby} exec #{cmd}}
|
75
|
+
puts command
|
76
|
+
next if o.dry_run
|
77
|
+
system(command) or errs << "ruby #{ruby}, rails #{rails}"
|
60
78
|
end
|
61
79
|
end
|
80
|
+
puts errs.any? ? "\n*** #{errs.size} failures:\n\t#{errs.join("\n\t")}" : "\n*** #{total > 1 ? 'all versions' : 'spec'} succeeded ***" unless o.dry_run
|
data/spec/migration_spec.rb
CHANGED
@@ -1,14 +1,6 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
3
3
|
|
4
|
-
describe ActiveRecord::Migration do
|
5
|
-
|
6
|
-
it "should respond to get_references" do
|
7
|
-
ActiveRecord::Migration.should respond_to :get_references
|
8
|
-
end
|
9
|
-
|
10
|
-
end
|
11
|
-
|
12
4
|
describe ActiveRecord::Migration do
|
13
5
|
include SchemaPlusHelpers
|
14
6
|
|
@@ -36,6 +28,16 @@ describe ActiveRecord::Migration do
|
|
36
28
|
@model.should reference(:users, :id).on(:user_id)
|
37
29
|
end
|
38
30
|
|
31
|
+
it "should create foreign key using t.references" do
|
32
|
+
create_table(@model, :user => {:METHOD => :references})
|
33
|
+
@model.should reference(:users, :id).on(:user_id)
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should not create foreign key using t.references with :references => nil" do
|
37
|
+
create_table(@model, :user => {:METHOD => :references, :references => nil})
|
38
|
+
@model.should_not reference(:users, :id).on(:user_id)
|
39
|
+
end
|
40
|
+
|
39
41
|
it "should create foreign key to the same table on parent_id" do
|
40
42
|
create_table(@model, :parent_id => {})
|
41
43
|
@model.should reference(@model.table_name, :id).on(:parent_id)
|
@@ -0,0 +1,138 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
3
|
+
|
4
|
+
describe ActiveRecord::Migration do
|
5
|
+
include SchemaPlusHelpers
|
6
|
+
|
7
|
+
before(:all) do
|
8
|
+
load_auto_schema
|
9
|
+
end
|
10
|
+
|
11
|
+
around(:each) do |example|
|
12
|
+
with_fk_config(:auto_create => true, :auto_index => true) { example.run }
|
13
|
+
end
|
14
|
+
|
15
|
+
context "when table is created" do
|
16
|
+
|
17
|
+
before(:each) do
|
18
|
+
@model = Post
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should create foreign keys" do
|
22
|
+
create_table(:user_id => {},
|
23
|
+
:author_id => { :references => :users },
|
24
|
+
:member_id => { :references => nil } )
|
25
|
+
@model.should reference(:users, :id).on(:user_id)
|
26
|
+
@model.should reference(:users, :id).on(:author_id)
|
27
|
+
@model.should_not reference.on(:member_id)
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
unless SchemaPlusHelpers.sqlite3?
|
33
|
+
|
34
|
+
context "when column is added" do
|
35
|
+
|
36
|
+
before(:each) do
|
37
|
+
@model = Comment
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should create a foreign key" do
|
41
|
+
add_column(:post_id, :integer) do
|
42
|
+
@model.should reference(:posts, :id).on(:post_id)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should create an index" do
|
47
|
+
add_column(:post_id, :integer) do
|
48
|
+
@model.should have_index.on(:post_id)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
context "when column is changed" do
|
55
|
+
|
56
|
+
before(:each) do
|
57
|
+
@model = Comment
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should create a foreign key" do
|
61
|
+
change_column :user, :string, :references => [:users, :login]
|
62
|
+
@model.should reference(:users, :login).on(:user)
|
63
|
+
change_column :user, :string, :references => nil
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
context "when column is removed" do
|
69
|
+
|
70
|
+
before(:each) do
|
71
|
+
@model = Comment
|
72
|
+
end
|
73
|
+
|
74
|
+
it "should remove a foreign key" do
|
75
|
+
suppress_messages do
|
76
|
+
target.add_column(@model.table_name, :post_id, :integer)
|
77
|
+
target.remove_column(@model.table_name, :post_id)
|
78
|
+
end
|
79
|
+
@model.should_not reference(:posts)
|
80
|
+
end
|
81
|
+
|
82
|
+
it "should remove an index" do
|
83
|
+
suppress_messages do
|
84
|
+
target.add_column(@model.table_name, :post_id, :integer)
|
85
|
+
target.remove_column(@model.table_name, :post_id)
|
86
|
+
end
|
87
|
+
@model.should_not have_index.on(:post_id)
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
|
94
|
+
protected
|
95
|
+
def target
|
96
|
+
ActiveRecord::Migration.connection
|
97
|
+
end
|
98
|
+
|
99
|
+
def add_column(column_name, *args)
|
100
|
+
table = @model.table_name
|
101
|
+
suppress_messages do
|
102
|
+
target.add_column(table, column_name, *args)
|
103
|
+
@model.reset_column_information
|
104
|
+
yield if block_given?
|
105
|
+
target.remove_column(table, column_name)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def change_column(column_name, *args)
|
110
|
+
table = @model.table_name
|
111
|
+
suppress_messages do
|
112
|
+
target.change_column(table, column_name, *args)
|
113
|
+
@model.reset_column_information
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def create_table(columns_with_options)
|
118
|
+
suppress_messages do
|
119
|
+
target.create_table @model.table_name, :force => true do |t|
|
120
|
+
columns_with_options.each_pair do |column, options|
|
121
|
+
t.send :integer, column, options
|
122
|
+
end
|
123
|
+
end
|
124
|
+
@model.reset_column_information
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def with_fk_config(opts, &block)
|
129
|
+
save = Hash[opts.keys.collect{|key| [key, SchemaPlus.config.foreign_keys.send(key)]}]
|
130
|
+
begin
|
131
|
+
SchemaPlus.config.foreign_keys.update_attributes(opts)
|
132
|
+
yield
|
133
|
+
ensure
|
134
|
+
SchemaPlus.config.foreign_keys.update_attributes(save)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|