dwilkie-foreigner 0.3

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 [name of plugin creator]
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,73 @@
1
+ Foreigner
2
+ =========
3
+
4
+ Adds limited SQLite support for Matt Higgins Foreigner.
5
+
6
+ Allows you to do the following in your migration files
7
+ t.foreign_key :posts
8
+
9
+ Which will generate the following SQL:
10
+ FOREIGN KEY ("post_id") REFERENCES "posts"(id)
11
+
12
+ To enforce the constraint use the genfkey tool included with SQLite:
13
+ rake db:migrate
14
+ sqlite3 db/development.sqlite3
15
+ .genfkey --exec
16
+
17
+ For more info see
18
+ http://www.sqlite.org/cvstrac/fileview?f=sqlite/tool/genfkey.README
19
+
20
+
21
+ Installation
22
+ ------------
23
+
24
+ Install as a plugin:
25
+
26
+ ruby script/plugin install git://github.com/dwilkie/foreigner.git
27
+
28
+ Install as a gem by adding the following to environment.rb:
29
+
30
+ config.gem "dwilkie-foreigner", :lib => "foreigner"
31
+
32
+ API
33
+ ---
34
+ t.foreign_key(options)
35
+ foreign_keys(table_name)
36
+
37
+ Since SQLite does not have complete ALTER TABLE support
38
+ you cannot use the following for an SQLite database
39
+
40
+ add_foreign_key(from_table, to_table, options)
41
+ remove_foreign_key(from_table, options)
42
+
43
+
44
+ Example
45
+ -------
46
+
47
+ The most common use of foreign keys is to reference a table that a model belongs to.
48
+ For example, given the following model:
49
+
50
+ class Comment < ActiveRecord::Base
51
+ belongs_to :post
52
+ end
53
+
54
+ class Post < ActiveRecord::Base
55
+ has_many :comments, :dependent => :delete_all
56
+ end
57
+
58
+ You should add a foreign key in your migration:
59
+
60
+ t.foreign_key :posts
61
+
62
+ If the column is named article_id instead of post_id, use the :column option:
63
+
64
+ t.foreign_key(:posts, :column => 'article_id')
65
+
66
+ schema.rb
67
+ ---------
68
+
69
+ Similar to indexes, the foreign keys in your database are automatically dumped to schema.rb.
70
+ This allows you to use foreign keys without fighting Rails!
71
+
72
+ Copyright (c) 2009 David Wilkie, released under the MIT license
73
+
data/Rakefile ADDED
@@ -0,0 +1,23 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ desc 'Default: run unit tests.'
6
+ task :default => :test
7
+
8
+ desc 'Test the foreigner plugin.'
9
+ Rake::TestTask.new(:test) do |t|
10
+ t.libs << 'lib'
11
+ t.libs << 'test'
12
+ t.pattern = 'test/**/*_test.rb'
13
+ t.verbose = true
14
+ end
15
+
16
+ desc 'Generate documentation for the foreigner plugin.'
17
+ Rake::RDocTask.new(:rdoc) do |rdoc|
18
+ rdoc.rdoc_dir = 'rdoc'
19
+ rdoc.title = 'Foreigner'
20
+ rdoc.options << '--line-numbers' << '--inline-source'
21
+ rdoc.rdoc_files.include('README')
22
+ rdoc.rdoc_files.include('lib/**/*.rb')
23
+ end
@@ -0,0 +1,151 @@
1
+ module Foreigner
2
+ module ConnectionAdapters
3
+ class ForeignKeyDefinition < Struct.new(:from_table, :to_table, :options) #:nodoc:
4
+ end
5
+
6
+ module SchemaDefinitions
7
+ def self.included(base)
8
+ base::TableDefinition.class_eval do
9
+ include Foreigner::ConnectionAdapters::TableDefinition
10
+ end
11
+
12
+ base::Table.class_eval do
13
+ include Foreigner::ConnectionAdapters::Table
14
+ end
15
+ end
16
+ end
17
+
18
+ module TableDefinition
19
+ class ForeignKey < Struct.new(:base, :to_table, :options)
20
+ def to_sql
21
+ base.foreign_key_definition(to_table, options)
22
+ end
23
+ alias to_s :to_sql
24
+ end
25
+
26
+ def self.included(base)
27
+ base.class_eval do
28
+ include InstanceMethods
29
+ alias_method_chain :references, :foreign_keys
30
+ alias_method_chain :to_sql, :foreign_keys
31
+ end
32
+ end
33
+
34
+ module InstanceMethods
35
+ # Adds a :foreign_key option to TableDefinition.references.
36
+ # If :foreign_key is true, a foreign key constraint is added to the table.
37
+ # You can also specify a hash, which is passed as foreign key options.
38
+ #
39
+ # ===== Examples
40
+ # ====== Add goat_id column and a foreign key to the goats table.
41
+ # t.references(:goat, :foreign_key => true)
42
+ # ====== Add goat_id column and a cascading foreign key to the goats table.
43
+ # t.references(:goat, :foreign_key => {:dependent => :delete})
44
+ #
45
+ # Note: No foreign key is created if :polymorphic => true is used.
46
+ # Note: If no name is specified, the database driver creates one for you!
47
+ def references_with_foreign_keys(*args)
48
+ options = args.extract_options!
49
+ fk_options = options.delete(:foreign_key)
50
+
51
+ if fk_options && !options[:polymorphic]
52
+ fk_options = {} if fk_options == true
53
+ args.each { |to_table| foreign_key(to_table, fk_options) }
54
+ end
55
+
56
+ references_without_foreign_keys(*(args << options))
57
+ end
58
+
59
+ # Defines a foreign key for the table. +to_table+ can be a single Symbol, or
60
+ # an Array of Symbols. See SchemaStatements#add_foreign_key
61
+ #
62
+ # ===== Examples
63
+ # ====== Creating a simple foreign key
64
+ # t.foreign_key(:people)
65
+ # ====== Defining the column
66
+ # t.foreign_key(:people, :column => :sender_id)
67
+ # ====== Creating a named foreign key
68
+ # t.foreign_key(:people, :column => :sender_id, :name => 'sender_foreign_key')
69
+ def foreign_key(to_table, options = {})
70
+ if @base.supports_foreign_keys?
71
+ to_table = to_table.to_s.pluralize if ActiveRecord::Base.pluralize_table_names
72
+ foreign_keys << ForeignKey.new(@base, to_table, options)
73
+ end
74
+ end
75
+
76
+ def to_sql_with_foreign_keys
77
+ sql = to_sql_without_foreign_keys
78
+ sql << ', ' << (foreign_keys * ', ') if foreign_keys.present?
79
+ sql
80
+ end
81
+
82
+ private
83
+ def foreign_keys
84
+ @foreign_keys ||= []
85
+ end
86
+ end
87
+ end
88
+
89
+ module Table
90
+ def self.included(base)
91
+ base.class_eval do
92
+ include InstanceMethods
93
+ alias_method_chain :references, :foreign_keys
94
+ end
95
+ end
96
+
97
+ module InstanceMethods
98
+ # Adds a new foreign key to the table. +to_table+ can be a single Symbol, or
99
+ # an Array of Symbols. See SchemaStatements#add_foreign_key
100
+ #
101
+ # ===== Examples
102
+ # ====== Creating a simple foreign key
103
+ # t.foreign_key(:people)
104
+ # ====== Defining the column
105
+ # t.foreign_key(:people, :column => :sender_id)
106
+ # ====== Creating a named foreign key
107
+ # t.foreign_key(:people, :column => :sender_id, :name => 'sender_foreign_key')
108
+ def foreign_key(to_table, options = {})
109
+ @base.add_foreign_key(@table_name, to_table, options)
110
+ end
111
+
112
+ # Remove the given foreign key from the table.
113
+ #
114
+ # ===== Examples
115
+ # ====== Remove the suppliers_company_id_fk in the suppliers table.
116
+ # t.remove_foreign_key :companies
117
+ # ====== Remove the foreign key named accounts_branch_id_fk in the accounts table.
118
+ # remove_foreign_key :column => :branch_id
119
+ # ====== Remove the foreign key named party_foreign_key in the accounts table.
120
+ # remove_index :name => :party_foreign_key
121
+ def remove_foreign_key(options = {})
122
+ @base.remove_foreign_key(@table_name, options)
123
+ end
124
+
125
+ # Adds a :foreign_key option to TableDefinition.references.
126
+ # If :foreign_key is true, a foreign key constraint is added to the table.
127
+ # You can also specify a hash, which is passed as foreign key options.
128
+ #
129
+ # ===== Examples
130
+ # ====== Add goat_id column and a foreign key to the goats table.
131
+ # t.references(:goat, :foreign_key => true)
132
+ # ====== Add goat_id column and a cascading foreign key to the goats table.
133
+ # t.references(:goat, :foreign_key => {:dependent => :delete})
134
+ #
135
+ # Note: No foreign key is created if :polymorphic => true is used.
136
+ def references_with_foreign_keys(*args)
137
+ options = args.extract_options!
138
+ polymorphic = options[:polymorphic]
139
+ fk_options = options.delete(:foreign_key)
140
+
141
+ references_without_foreign_keys(*(args << options))
142
+
143
+ if fk_options && !polymorphic
144
+ fk_options = {} if fk_options == true
145
+ args.each { |to_table| foreign_key(to_table, fk_options) }
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,72 @@
1
+ module Foreigner
2
+ module ConnectionAdapters
3
+ module SchemaStatements
4
+ def self.included(base)
5
+ base::AbstractAdapter.class_eval do
6
+ include Foreigner::ConnectionAdapters::AbstractAdapter
7
+ end
8
+ end
9
+ end
10
+
11
+ module AbstractAdapter
12
+ def supports_foreign_keys?
13
+ false
14
+ end
15
+
16
+ # Adds a new foreign key to the +from_table+, referencing the primary key of +to_table+
17
+ #
18
+ # The foreign key will be named after the from and to tables unless you pass
19
+ # <tt>:name</tt> as an option.
20
+ #
21
+ # ===== Examples
22
+ # ====== Creating a foreign key
23
+ # add_foreign_key(:comments, :posts)
24
+ # generates
25
+ # ALTER TABLE `comments` ADD CONSTRAINT
26
+ # `comments_post_id_fk` FOREIGN KEY (`post_id`) REFERENCES `posts` (`id`)
27
+ #
28
+ # ====== Creating a named foreign key
29
+ # add_foreign_key(:comments, :posts, :name => 'comments_belongs_to_posts')
30
+ # generates
31
+ # ALTER TABLE `comments` ADD CONSTRAINT
32
+ # `comments_belongs_to_posts` FOREIGN KEY (`post_id`) REFERENCES `posts` (`id`)
33
+ #
34
+ # ====== Creating a cascading foreign_key on a custom column
35
+ # add_foreign_key(:people, :people, :column => 'best_friend_id', :dependent => :nullify)
36
+ # generates
37
+ # ALTER TABLE `people` ADD CONSTRAINT
38
+ # `people_best_friend_id_fk` FOREIGN KEY (`best_friend_id`) REFERENCES `people` (`id`)
39
+ # ON DELETE SET NULL
40
+ #
41
+ # === Supported options
42
+ # [:column]
43
+ # Specify the column name on the from_table that references the to_table. By default this is guessed
44
+ # to be the singular name of the to_table with "_id" suffixed. So a to_table of :posts will use "post_id"
45
+ # as the default <tt>:column</tt>.
46
+ # [:name]
47
+ # Specify the name of the foreign key constraint. This defaults to use from_table and foreign key column.
48
+ # [:dependent]
49
+ # If set to <tt>:delete</tt>, the associated records in from_table are deleted when records in to_table table are deleted.
50
+ # If set to <tt>:nullify</tt>, the foreign key column is set to +NULL+.
51
+ def add_foreign_key(from_table, to_table, options = {})
52
+ end
53
+
54
+ # Remove the given foreign key from the table.
55
+ #
56
+ # ===== Examples
57
+ # ====== Remove the suppliers_company_id_fk in the suppliers table.
58
+ # remove_foreign_key :suppliers, :companies
59
+ # ====== Remove the foreign key named accounts_branch_id_fk in the accounts table.
60
+ # remove_foreign_key :accounts, :column => :branch_id
61
+ # ====== Remove the foreign key named party_foreign_key in the accounts table.
62
+ # remove_foreign_key :accounts, :name => :party_foreign_key
63
+ def remove_foreign_key(from_table, options)
64
+ end
65
+
66
+ # Return the foreign keys for the schema_dumper
67
+ def foreign_keys(table_name)
68
+ []
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,46 @@
1
+ require 'foreigner/connection_adapters/sql_2003'
2
+
3
+ module Foreigner
4
+ module ConnectionAdapters
5
+ module MysqlAdapter
6
+ include Foreigner::ConnectionAdapters::Sql2003
7
+
8
+ def foreign_keys(table_name)
9
+ foreign_keys = []
10
+ fk_info = select_all %{
11
+ SELECT fk.referenced_table_name as 'to_table'
12
+ ,fk.column_name as 'column'
13
+ ,fk.constraint_name as 'name'
14
+ FROM information_schema.key_column_usage fk
15
+ WHERE fk.referenced_column_name is not null
16
+ AND fk.table_schema = '#{@config[:database]}'
17
+ AND fk.table_name = '#{table_name}'
18
+ }
19
+
20
+ create_table_info = select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"]
21
+
22
+ fk_info.each do |row|
23
+ options = {:column => row['column'], :name => row['name']}
24
+ if create_table_info =~ /CONSTRAINT #{quote_column_name(row['name'])} FOREIGN KEY .* REFERENCES .* ON DELETE (CASCADE|SET NULL)/
25
+ if $1 == 'CASCADE'
26
+ options[:dependent] = :delete
27
+ elsif $1 == 'SET NULL'
28
+ options[:dependent] = :nullify
29
+ end
30
+ end
31
+ foreign_keys << ForeignKeyDefinition.new(table_name, row['to_table'], options)
32
+ end
33
+
34
+ foreign_keys
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ module ActiveRecord
41
+ module ConnectionAdapters
42
+ MysqlAdapter.class_eval do
43
+ include Foreigner::ConnectionAdapters::MysqlAdapter
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,46 @@
1
+ require 'foreigner/connection_adapters/sql_2003'
2
+
3
+ module Foreigner
4
+ module ConnectionAdapters
5
+ module PostgreSQLAdapter
6
+ include Foreigner::ConnectionAdapters::Sql2003
7
+
8
+ def foreign_keys(table_name)
9
+ fk_info = select_all %{
10
+ select tc.constraint_name as name
11
+ ,ccu.table_name as to_table
12
+ ,kcu.column_name as column
13
+ ,rc.delete_rule as dependency
14
+ from information_schema.table_constraints tc
15
+ join information_schema.key_column_usage kcu
16
+ using (constraint_catalog, constraint_schema, constraint_name)
17
+ join information_schema.referential_constraints rc
18
+ using (constraint_catalog, constraint_schema, constraint_name)
19
+ join information_schema.constraint_column_usage ccu
20
+ using (constraint_catalog, constraint_schema, constraint_name)
21
+ where tc.constraint_type = 'FOREIGN KEY'
22
+ and tc.constraint_catalog = '#{@config[:database]}'
23
+ and tc.table_name = '#{table_name}'
24
+ }
25
+
26
+ fk_info.inject([]) do |foreign_keys, row|
27
+ options = {:column => row['column'], :name => row['name']}
28
+ if row['dependency'] == 'CASCADE'
29
+ options[:dependent] = :delete
30
+ elsif row['dependency'] == 'SET NULL'
31
+ options[:dependent] = :nullify
32
+ end
33
+ foreign_keys << ForeignKeyDefinition.new(table_name, row['to_table'], options)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ module ActiveRecord
41
+ module ConnectionAdapters
42
+ PostgreSQLAdapter.class_eval do
43
+ include Foreigner::ConnectionAdapters::PostgreSQLAdapter
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,57 @@
1
+ module Foreigner
2
+ module ConnectionAdapters
3
+ module Sql2003
4
+ def supports_foreign_keys?
5
+ true
6
+ end
7
+
8
+ def add_foreign_key(from_table, to_table, options = {})
9
+ column = options[:column] || "#{to_table.to_s.singularize}_id"
10
+ foreign_key_name = foreign_key_name(from_table, column, options)
11
+
12
+ sql =
13
+ "ALTER TABLE #{quote_table_name(from_table)} " +
14
+ "ADD CONSTRAINT #{quote_column_name(foreign_key_name)} " +
15
+ foreign_key_definition(to_table, options)
16
+
17
+ execute(sql)
18
+ end
19
+
20
+ def foreign_key_definition(to_table, options = {})
21
+ column = options[:column] || "#{to_table.to_s.singularize}_id"
22
+ dependency = dependency_sql(options[:dependent])
23
+
24
+ sql = "FOREIGN KEY (#{quote_column_name(column)}) REFERENCES #{quote_table_name(to_table)}(id)"
25
+ sql << " #{dependency}" unless dependency.blank?
26
+ sql
27
+ end
28
+
29
+ def remove_foreign_key(table, options)
30
+ if Hash === options
31
+ foreign_key_name = foreign_key_name(table, options[:column], options)
32
+ else
33
+ foreign_key_name = foreign_key_name(table, "#{options.to_s.singularize}_id")
34
+ end
35
+
36
+ execute "ALTER TABLE #{quote_table_name(table)} DROP FOREIGN KEY #{quote_column_name(foreign_key_name)}"
37
+ end
38
+
39
+ private
40
+ def foreign_key_name(table, column, options = {})
41
+ if options[:name]
42
+ options[:name]
43
+ else
44
+ "#{table}_#{column}_fk"
45
+ end
46
+ end
47
+
48
+ def dependency_sql(dependency)
49
+ case dependency
50
+ when :nullify then "ON DELETE SET NULL"
51
+ when :delete then "ON DELETE CASCADE"
52
+ else ""
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,38 @@
1
+ require 'foreigner/connection_adapters/sql_2003'
2
+
3
+ module Foreigner
4
+ module ConnectionAdapters
5
+ module SQLiteAdapter
6
+ include Foreigner::ConnectionAdapters::Sql2003
7
+
8
+ def foreign_keys(table_name)
9
+ foreign_keys = []
10
+ create_table_info = select_value %{
11
+ SELECT sql
12
+ FROM sqlite_master
13
+ WHERE sql LIKE '%FOREIGN KEY%'
14
+ AND name = '#{table_name}'
15
+ }
16
+ if !create_table_info.nil?
17
+ fk_columns = create_table_info.scan(/FOREIGN KEY\s*\(\"([^\"]+)\"\)/)
18
+ fk_tables = create_table_info.scan(/REFERENCES\s*\"([^\"]+)\"/)
19
+ if fk_columns.size == fk_tables.size
20
+ fk_columns.each_with_index do |fk_column, index|
21
+ foreign_keys << ForeignKeyDefinition.new(table_name, fk_tables[index][0], :column => fk_column[0])
22
+ end
23
+ end
24
+ end
25
+ foreign_keys
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ module ActiveRecord
32
+ module ConnectionAdapters
33
+ SQLiteAdapter.class_eval do
34
+ include Foreigner::ConnectionAdapters::SQLiteAdapter
35
+ end
36
+ end
37
+ end
38
+
@@ -0,0 +1,147 @@
1
+ module Foreigner
2
+ module SchemaDumper
3
+ def self.included(base)
4
+ base.class_eval do
5
+ include InstanceMethods
6
+ alias_method_chain :tables, :foreign_keys
7
+ alias_method_chain :table, :foreign_keys
8
+ end
9
+ end
10
+
11
+ module InstanceMethods
12
+ def tables_with_foreign_keys(stream)
13
+ tables_without_foreign_keys(stream)
14
+ if @connection.class != ActiveRecord::ConnectionAdapters::SQLite3Adapter
15
+ @connection.tables.sort.each do |table|
16
+ foreign_keys(table, stream)
17
+ end
18
+ end
19
+ end
20
+
21
+ def table_with_foreign_keys(table, stream)
22
+ if @connection.class == ActiveRecord::ConnectionAdapters::SQLite3Adapter
23
+ foreign_key_table(table, stream)
24
+ else
25
+ table_without_foreign_keys(table, stream)
26
+ end
27
+ end
28
+
29
+ private
30
+ def foreign_keys(table_name, stream)
31
+ debugger
32
+ if (foreign_keys = @connection.foreign_keys(table_name)).any?
33
+ add_foreign_key_statements = foreign_keys.map do |foreign_key|
34
+ statement_parts = [ ('add_foreign_key ' + foreign_key.from_table.inspect) ]
35
+ statement_parts << foreign_key.to_table.inspect
36
+ statement_parts << (':column => ' + foreign_key.options[:column].inspect)
37
+ statement_parts << (':name => ' + foreign_key.options[:name].inspect)
38
+ if foreign_key.options[:dependent].present?
39
+ statement_parts << (':dependent => ' + foreign_key.options[:dependent].inspect)
40
+ end
41
+
42
+ ' ' + statement_parts.join(', ')
43
+ end
44
+
45
+ stream.puts add_foreign_key_statements.sort.join("\n")
46
+ stream.puts
47
+ end
48
+ end
49
+
50
+ # This is a direct copy from
51
+ # active_record/schema.dumper
52
+ def foreign_key_table(table, stream)
53
+ columns = @connection.columns(table)
54
+ begin
55
+ tbl = StringIO.new
56
+
57
+ # first dump primary key column
58
+ if @connection.respond_to?(:pk_and_sequence_for)
59
+ pk, pk_seq = @connection.pk_and_sequence_for(table)
60
+ elsif @connection.respond_to?(:primary_key)
61
+ pk = @connection.primary_key(table)
62
+ end
63
+ pk ||= 'id'
64
+
65
+ tbl.print " create_table #{table.inspect}"
66
+ if columns.detect { |c| c.name == pk }
67
+ if pk != 'id'
68
+ tbl.print %Q(, :primary_key => "#{pk}")
69
+ end
70
+ else
71
+ tbl.print ", :id => false"
72
+ end
73
+ tbl.print ", :force => true"
74
+ tbl.puts " do |t|"
75
+
76
+ # then dump all non-primary key columns
77
+ column_specs = columns.map do |column|
78
+ raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" if @types[column.type].nil?
79
+ next if column.name == pk
80
+ spec = {}
81
+ spec[:name] = column.name.inspect
82
+ spec[:type] = column.type.to_s
83
+ spec[:limit] = column.limit.inspect if column.limit != @types[column.type][:limit] && column.type != :decimal
84
+ spec[:precision] = column.precision.inspect if !column.precision.nil?
85
+ spec[:scale] = column.scale.inspect if !column.scale.nil?
86
+ spec[:null] = 'false' if !column.null
87
+ spec[:default] = default_string(column.default) if column.has_default?
88
+ (spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k.inspect} => ")}
89
+ spec
90
+ end.compact
91
+
92
+ # find all migration keys used in this table
93
+ keys = [:name, :limit, :precision, :scale, :default, :null] & column_specs.map(&:keys).flatten
94
+
95
+ # figure out the lengths for each column based on above keys
96
+ lengths = keys.map{ |key| column_specs.map{ |spec| spec[key] ? spec[key].length + 2 : 0 }.max }
97
+
98
+ # the string we're going to sprintf our values against, with standardized column widths
99
+ format_string = lengths.map{ |len| "%-#{len}s" }
100
+
101
+ # find the max length for the 'type' column, which is special
102
+ type_length = column_specs.map{ |column| column[:type].length }.max
103
+
104
+ # add column type definition to our format string
105
+ format_string.unshift " t.%-#{type_length}s "
106
+
107
+ format_string *= ''
108
+
109
+ column_specs.each do |colspec|
110
+ values = keys.zip(lengths).map{ |key, len| colspec.key?(key) ? colspec[key] + ", " : " " * len }
111
+ values.unshift colspec[:type]
112
+ tbl.print((format_string % values).gsub(/,\s*$/, ''))
113
+ tbl.puts
114
+ end
115
+
116
+ add_foreign_keys(table, tbl)
117
+
118
+ tbl.puts " end"
119
+ tbl.puts
120
+
121
+ indexes(table, tbl)
122
+
123
+ tbl.rewind
124
+ stream.print tbl.read
125
+ rescue => e
126
+ stream.puts "# Could not dump table #{table.inspect} because of following #{e.class}"
127
+ stream.puts "# #{e.message}"
128
+ stream.puts
129
+ end
130
+
131
+ stream
132
+ end
133
+
134
+ def add_foreign_keys(table_name, stream)
135
+ if (foreign_keys = @connection.foreign_keys(table_name)).any?
136
+ add_foreign_key_statements = foreign_keys.map do |foreign_key|
137
+ statement_parts = [" t.foreign_key " + foreign_key.to_table.inspect]
138
+ statement_parts << (':column => ' + foreign_key.options[:column].inspect)
139
+ ' ' + statement_parts.join(', ')
140
+ end
141
+
142
+ stream.puts add_foreign_key_statements.sort.join("\n")
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
data/lib/foreigner.rb ADDED
@@ -0,0 +1,20 @@
1
+ require 'foreigner/connection_adapters/abstract/schema_statements'
2
+ require 'foreigner/connection_adapters/abstract/schema_definitions'
3
+ require 'foreigner/schema_dumper'
4
+
5
+ module ActiveRecord
6
+ module ConnectionAdapters
7
+ include Foreigner::ConnectionAdapters::SchemaStatements
8
+ include Foreigner::ConnectionAdapters::SchemaDefinitions
9
+ end
10
+
11
+ SchemaDumper.class_eval do
12
+ include Foreigner::SchemaDumper
13
+ end
14
+
15
+ Base.class_eval do
16
+ if ['MySQL', 'PostgreSQL', 'SQLite'].include? connection.adapter_name
17
+ require "foreigner/connection_adapters/#{connection.adapter_name.downcase}_adapter"
18
+ end
19
+ end
20
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,8 @@
1
+ require 'test/unit'
2
+ require 'rubygems'
3
+ require 'active_support'
4
+ require 'active_support/test_case'
5
+ require 'active_record'
6
+ require 'active_record/test_case'
7
+ require 'active_record/connection_adapters/mysql_adapter'
8
+ require 'foreigner'
@@ -0,0 +1,83 @@
1
+ require File.dirname(__FILE__) + '/helper'
2
+
3
+ class MysqlAdapterTest < ActiveRecord::TestCase
4
+ include Foreigner::MysqlAdapter
5
+
6
+ def test_add_without_options
7
+ assert_equal(
8
+ "ALTER TABLE `employees` ADD CONSTRAINT `employees_company_id_fk` FOREIGN KEY (`company_id`) REFERENCES `companies`(id)",
9
+ add_foreign_key(:employees, :companies)
10
+ )
11
+ end
12
+
13
+ def test_add_with_name
14
+ assert_equal(
15
+ "ALTER TABLE `employees` ADD CONSTRAINT `favorite_company_fk` FOREIGN KEY (`company_id`) REFERENCES `companies`(id)",
16
+ add_foreign_key(:employees, :companies, :name => 'favorite_company_fk')
17
+ )
18
+ end
19
+
20
+ def test_add_with_column
21
+ assert_equal(
22
+ "ALTER TABLE `employees` ADD CONSTRAINT `employees_last_employer_id_fk` FOREIGN KEY (`last_employer_id`) REFERENCES `companies`(id)",
23
+ add_foreign_key(:employees, :companies, :column => 'last_employer_id')
24
+ )
25
+ end
26
+
27
+ def test_add_with_column_and_name
28
+ assert_equal(
29
+ "ALTER TABLE `employees` ADD CONSTRAINT `favorite_company_fk` FOREIGN KEY (`last_employer_id`) REFERENCES `companies`(id)",
30
+ add_foreign_key(:employees, :companies, :column => 'last_employer_id', :name => 'favorite_company_fk')
31
+ )
32
+ end
33
+
34
+ def test_add_with_delete_dependency
35
+ assert_equal(
36
+ "ALTER TABLE `employees` ADD CONSTRAINT `employees_company_id_fk` FOREIGN KEY (`company_id`) REFERENCES `companies`(id) " +
37
+ "ON DELETE CASCADE",
38
+ add_foreign_key(:employees, :companies, :dependent => :delete)
39
+ )
40
+ end
41
+
42
+ def test_add_with_nullify_dependency
43
+ assert_equal(
44
+ "ALTER TABLE `employees` ADD CONSTRAINT `employees_company_id_fk` FOREIGN KEY (`company_id`) REFERENCES `companies`(id) " +
45
+ "ON DELETE SET NULL",
46
+ add_foreign_key(:employees, :companies, :dependent => :nullify)
47
+ )
48
+ end
49
+
50
+ def test_remove_by_table
51
+ assert_equal(
52
+ "ALTER TABLE `suppliers` DROP FOREIGN KEY `suppliers_company_id_fk`",
53
+ remove_foreign_key(:suppliers, :companies)
54
+ )
55
+ end
56
+
57
+ def test_remove_by_name
58
+ assert_equal(
59
+ "ALTER TABLE `suppliers` DROP FOREIGN KEY `belongs_to_supplier`",
60
+ remove_foreign_key(:suppliers, :name => "belongs_to_supplier")
61
+ )
62
+ end
63
+
64
+ def test_remove_by_column
65
+ assert_equal(
66
+ "ALTER TABLE `suppliers` DROP FOREIGN KEY `suppliers_ship_to_id_fk`",
67
+ remove_foreign_key(:suppliers, :column => "ship_to_id")
68
+ )
69
+ end
70
+
71
+ private
72
+ def execute(sql, name = nil)
73
+ sql
74
+ end
75
+
76
+ def quote_column_name(name)
77
+ "`#{name}`"
78
+ end
79
+
80
+ def quote_table_name(name)
81
+ quote_column_name(name).gsub('.', '`.`')
82
+ end
83
+ end
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dwilkie-foreigner
3
+ version: !ruby/object:Gem::Version
4
+ version: "0.3"
5
+ platform: ruby
6
+ authors:
7
+ - David Wilkie
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-09-16 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: Foreign keys for Rails migrations with limited SQLite support
17
+ email: dwilkie@gmail.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README
24
+ files:
25
+ - MIT-LICENSE
26
+ - Rakefile
27
+ - README
28
+ - lib/foreigner.rb
29
+ - lib/foreigner
30
+ - lib/foreigner/schema_dumper.rb
31
+ - lib/foreigner/connection_adapters
32
+ - lib/foreigner/connection_adapters/sql_2003.rb
33
+ - lib/foreigner/connection_adapters/mysql_adapter.rb
34
+ - lib/foreigner/connection_adapters/postgresql_adapter.rb
35
+ - lib/foreigner/connection_adapters/sqlite_adapter.rb
36
+ - lib/foreigner/connection_adapters/abstract/schema_definitions.rb
37
+ - lib/foreigner/connection_adapters/abstract/schema_statements.rb
38
+ - test/helper.rb
39
+ - test/mysql_adapter_test.rb
40
+ has_rdoc: false
41
+ homepage: http://github.com/dwilkie/foreigner/tree/master
42
+ licenses:
43
+ post_install_message:
44
+ rdoc_options:
45
+ - --line-numbers
46
+ - --main
47
+ - README
48
+ require_paths:
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: "0"
55
+ version:
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: "0"
61
+ version:
62
+ requirements: []
63
+
64
+ rubyforge_project: foreigner
65
+ rubygems_version: 1.3.5
66
+ signing_key:
67
+ specification_version: 1
68
+ summary: Foreign keys for Rails migrations with limited SQLite support
69
+ test_files: []
70
+