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.
@@ -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
@@ -1,3 +1,3 @@
1
1
  module SchemaPlus
2
- VERSION = "0.1.2"
2
+ VERSION = "0.1.3"
3
3
  end
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 = RUBY_VERSIONS
11
+ o.ruby_versions = ["1.9.2"]
13
12
  o.rails_versions = RAILS_VERSIONS
14
- o.run_cmd = "rake spec"
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: postgresql, mysql, mysql2, or sqlite3. Default is all of them" ) do |adapter|
24
- p adapter
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.run_cmd = "rake postgresql:spec"
51
+ o.db_adapters = ["postgresql"]
40
52
  end
41
53
 
42
54
  end.parse!
43
55
 
44
- cmds = if o.install
45
- "bundle install"
46
- else
47
- "bundle exec #{o.run_cmd}"
48
- end
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 #{cmds}}
59
- system(command)
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
@@ -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