schema_plus 0.1.2 → 0.1.3
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/.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
|