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.
@@ -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