sparkfly-foreigner 0.5.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,48 @@
1
+ module Foreigner
2
+ module ConnectionAdapters
3
+ module MysqlAdapter
4
+ include Foreigner::Semantics::Sql2003
5
+
6
+
7
+ # Override SQL2003 Semantics for MySQL
8
+ def sql_for_remove_foreign_key(table, foreign_key_name)
9
+ "ALTER TABLE #{quote_table_name(table)} DROP FOREIGN KEY #{quote_column_name(foreign_key_name)}"
10
+ end
11
+
12
+ def foreign_keys(table_name)
13
+ fk_info = select_all %{
14
+ SELECT fk.referenced_table_name as 'to_table'
15
+ ,fk.referenced_column_name as 'primary_key'
16
+ ,fk.column_name as 'column'
17
+ ,fk.constraint_name as 'name'
18
+ FROM information_schema.key_column_usage fk
19
+ WHERE fk.referenced_column_name is not null
20
+ AND fk.table_schema = '#{@config[:database]}'
21
+ AND fk.table_name = '#{table_name}'
22
+ }
23
+
24
+ create_table_info = select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"]
25
+
26
+ fk_info.map do |row|
27
+ options = {:column => row['column'], :name => row['name'], :primary_key => row['primary_key']}
28
+
29
+ if create_table_info =~ /CONSTRAINT #{quote_column_name(row['name'])} FOREIGN KEY .* REFERENCES .* ON DELETE (CASCADE|SET NULL)/
30
+ options[:dependent] = case $1
31
+ when 'CASCADE' then :delete
32
+ when 'SET NULL' then :nullify
33
+ end
34
+ end
35
+ ForeignKeyDefinition.new(table_name.to_s, row['to_table'], options)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ module ActiveRecord
43
+ module ConnectionAdapters
44
+ MysqlAdapter.class_eval do
45
+ include Foreigner::ConnectionAdapters::MysqlAdapter
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,47 @@
1
+ module Foreigner
2
+ module ConnectionAdapters
3
+ module PostgreSQLAdapter
4
+ include Foreigner::Semantics::Sql2003
5
+
6
+
7
+ def foreign_keys(table_name)
8
+ fk_info = select_all %{
9
+ SELECT tc.constraint_name as name
10
+ ,ccu.table_name as to_table
11
+ ,ccu.column_name as primary_key
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.map do |row|
27
+ options = {:column => row['column'], :name => row['name'], :primary_key => row['primary_key']}
28
+
29
+ options[:dependent] = case row['dependency']
30
+ when 'CASCADE' then :delete
31
+ when 'SET NULL' then :nullify
32
+ end
33
+
34
+ ForeignKeyDefinition.new(table_name.to_s, row['to_table'], options)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ module ActiveRecord
42
+ module ConnectionAdapters
43
+ PostgreSQLAdapter.class_eval do
44
+ include Foreigner::ConnectionAdapters::PostgreSQLAdapter
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,46 @@
1
+ require 'foreigner/connection_adapters/sql_2003'
2
+
3
+ module Foreigner
4
+ module ConnectionAdapters
5
+ module SQLite3Adapter
6
+ include Foreigner::Semantics::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
+ unless create_table_info.nil?
17
+ fk_columns = create_table_info.scan(/FOREIGN KEY\s*\(\"([^\"]+)\"\)/)
18
+ fk_tables = create_table_info.scan(/REFERENCES\s*\"([^\"]+)\"/)
19
+ fk_references = create_table_info.scan(/REFERENCES[^\,]+/)
20
+ if fk_columns.size == fk_tables.size && fk_references.size == fk_columns.size
21
+ fk_columns.each_with_index do |fk_column, index|
22
+ if fk_references[index] =~ /ON DELETE CASCADE/
23
+ fk_references[index] = :delete
24
+ elsif fk_references[index] =~ /ON DELETE SET NULL/
25
+ fk_references[index] = :nullify
26
+ else
27
+ fk_references[index] = nil
28
+ end
29
+ foreign_keys << ForeignKeyDefinition.new(table_name, fk_tables[index][0], :column => fk_column[0], :dependent => fk_references[index])
30
+ end
31
+ end
32
+ end
33
+ foreign_keys
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ module ActiveRecord
40
+ module ConnectionAdapters
41
+ SQLite3Adapter.class_eval do
42
+ include Foreigner::ConnectionAdapters::SQLite3Adapter
43
+ end
44
+ end
45
+ end
46
+
@@ -0,0 +1,47 @@
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
+ end
8
+ end
9
+
10
+ module InstanceMethods
11
+ def tables_with_foreign_keys(stream)
12
+ tables_without_foreign_keys(stream)
13
+ @connection.tables.sort.each do |table|
14
+ next unless foreign_keys = @connection.foreign_keys(table)
15
+ stream.puts generate_foreign_keys_statements(foreign_keys).join("\n")
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ # Generates a string for a given list of ForeignKeyDefinition
22
+ # Has no concept of streams or connections, so this can be tested in isolation.
23
+ def generate_foreign_keys_statements(foreign_keys)
24
+ decorator = [
25
+ # [ :option_name, lambda { |fk| filter } ],
26
+ [ :name, lambda { |fk| fk.options[:name] } ],
27
+ [ :column, lambda { |fk| fk.options[:column] && fk.options[:column] != "#{fk.to_table.singularize}_id" } ],
28
+ [ :primary_key, lambda { |fk| fk.options[:primary_key] && fk.options[:primary_key] != 'id' } ],
29
+ [ :dependent, lambda { |fk| fk.options[:dependent].present? } ]
30
+ ]
31
+
32
+ foreign_keys.map do |foreign_key|
33
+ statement_parts = [[ ' ', 'add_foreign_key', foreign_key.from_table.to_sym.inspect].join(' ') ]
34
+ statement_parts << foreign_key.to_table.to_sym.inspect
35
+
36
+ if foreign_key.options
37
+ statement_parts << decorator.map do |option, guard|
38
+ [ ':', option, ' => ', foreign_key.options[option].inspect ].join if guard.call(foreign_key)
39
+ end - [nil]
40
+ end
41
+ ' ' + statement_parts.join(', ')
42
+ end
43
+ end
44
+ end # InstanceMethods
45
+
46
+ end
47
+ end
@@ -0,0 +1,78 @@
1
+ module Foreigner
2
+ module Semantics
3
+ module Sql2003
4
+ def supports_foreign_keys?
5
+ true
6
+ end
7
+
8
+ def foreign_key_definition(to_table, options = {})
9
+ column = options[:column] || "#{to_table.to_s.singularize}_id"
10
+ dependency = sql_for_dependency(options[:dependent])
11
+
12
+ sql = "FOREIGN KEY (#{quote_column_name(column)}) REFERENCES #{quote_table_name(to_table)}(id)"
13
+ sql << " #{dependency}" unless dependency.blank?
14
+ sql
15
+ end
16
+
17
+ def add_foreign_key(from_table, to_table, options = {})
18
+ column = options[:column] || "#{to_table.to_s.singularize}_id"
19
+ foreign_key_name = foreign_key_name(from_table, column, options)
20
+ primary_key = options[:primary_key] || "id"
21
+ reference = sql_for_reference(to_table, primary_key)
22
+ dependency = sql_for_dependency(options[:dependent])
23
+
24
+ execute(sql_for_add_foreign_key(from_table, foreign_key_name, column, reference, dependency))
25
+ end
26
+
27
+ def remove_foreign_key(table, options)
28
+ # If the second argument is table name (String/Symbol) then convert that to the
29
+ # to the full options hash
30
+ options = { :column => column_name(options) } if String === options || Symbol === options
31
+ execute(sql_for_remove_foreign_key(table, foreign_key_name(table, options[:column], options)))
32
+ end
33
+
34
+ private
35
+
36
+ def foreign_key_name(table, column, options = {})
37
+ return options[:name] if options[:name]
38
+ "fk_#{table}_#{column}"
39
+ end
40
+
41
+ def column_name(column)
42
+ "#{column.to_s.singularize}_id"
43
+ end
44
+
45
+ # Generates SQL and returns it.
46
+ def sql_for_add_foreign_key(from_table, foreign_key_name, column, reference, dependent = nil)
47
+ sql = [
48
+ "ALTER TABLE #{quote_table_name(from_table)}",
49
+ "ADD CONSTRAINT #{quote_column_name(foreign_key_name)}",
50
+ "FOREIGN KEY (#{quote_column_name(column)})",
51
+ "REFERENCES #{reference}"
52
+ ]
53
+
54
+ sql << "#{dependent}" unless dependent.blank?
55
+ sql.join(' ')
56
+ end
57
+
58
+ def sql_for_remove_foreign_key(table, foreign_key_name)
59
+ "ALTER TABLE #{quote_table_name(table)} DROP CONSTRAINT #{quote_column_name(foreign_key_name)}"
60
+ end
61
+
62
+ def sql_for_reference(to_table, primary_key)
63
+ "#{quote_table_name(ActiveRecord::Migrator.proper_table_name(to_table))}(#{primary_key})"
64
+ end
65
+
66
+
67
+ def sql_for_dependency(dependency)
68
+ case dependency
69
+ when :nullify then 'ON DELETE SET NULL'
70
+ when :delete then 'ON DELETE CASCADE'
71
+ else ''
72
+ end
73
+ end
74
+
75
+ end
76
+ end
77
+ end
78
+
@@ -0,0 +1,100 @@
1
+
2
+ # CONFIGURATIONS is defined in spec_helper
3
+
4
+ module AdapterHelper
5
+ module AdapterTestHarness
6
+ def recreate_test_environment(env)
7
+ ActiveRecord::Base.establish_connection(CONFIGURATIONS[env])
8
+
9
+ @database = CONFIGURATIONS[env][:database]
10
+ ActiveRecord::Base.connection.drop_database(@database)
11
+ ActiveRecord::Base.connection.create_database(@database)
12
+ ActiveRecord::Base.connection.reset!
13
+
14
+ FactoryHelpers::CreateCollection.up
15
+ end
16
+
17
+ def schema(table_name)
18
+ raise 'This method must be overridden'
19
+ end
20
+
21
+ def foreign_keys(table)
22
+ ActiveRecord::Base.connection.foreign_keys(table)
23
+ end
24
+
25
+ private
26
+
27
+ def execute(sql, name = nil)
28
+ sql
29
+ end
30
+
31
+ def quote_column_name(name)
32
+ "`#{name}`"
33
+ end
34
+
35
+ def quote_table_name(name)
36
+ quote_column_name(name).gsub('.', '`.`')
37
+ end
38
+
39
+ end
40
+
41
+ class PostgreSQLTestAdapter
42
+ include Foreigner::ConnectionAdapters::PostgreSQLAdapter
43
+ include AdapterTestHarness
44
+
45
+ def recreate_test_environment
46
+ ActiveRecord::Base.establish_connection(CONFIGURATIONS[:postgresql_admin])
47
+ @database = CONFIGURATIONS[:postgresql][:database]
48
+
49
+ ActiveRecord::Base.connection.drop_database(@database)
50
+ ActiveRecord::Base.connection.create_database(@database)
51
+
52
+ ActiveRecord::Base.connection.disconnect!
53
+ ActiveRecord::Base.establish_connection(CONFIGURATIONS[:postgresql])
54
+
55
+ FactoryHelpers::CreateCollection.up
56
+ end
57
+ end
58
+
59
+ class MySQLTestAdapter
60
+ include Foreigner::ConnectionAdapters::MysqlAdapter
61
+ include AdapterTestHarness
62
+
63
+ def schema(table_name)
64
+ ActiveRecord::Base.connection.select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"]
65
+ end
66
+
67
+ def recreate_test_environment
68
+ super(:mysql)
69
+ end
70
+ end
71
+
72
+ class SQLite3TestAdapter
73
+ include Foreigner::ConnectionAdapters::SQLite3Adapter
74
+ include AdapterTestHarness
75
+
76
+ def schema(table_name) ActiveRecord::Base.connection.select_value %{
77
+ SELECT sql
78
+ FROM sqlite_master
79
+ WHERE name = '#{table_name}'
80
+ }
81
+ end
82
+
83
+ def foreign_keys(table)
84
+ raise "Unimplemented"
85
+ end
86
+
87
+ def recreate_test_environment
88
+ ActiveRecord::Base.establish_connection(CONFIGURATIONS[:sqlite3])
89
+
90
+ @database = CONFIGURATIONS[:sqlite3][:database]
91
+ #ActiveRecord::Base.connection.drop_database(@database)
92
+ #ActiveRecord::Base.connection.create_database(@database)
93
+ ActiveRecord::Base.connection.reset!
94
+
95
+ FactoryHelpers::CreateCollection.up
96
+ end
97
+ end
98
+
99
+
100
+ end
@@ -0,0 +1,70 @@
1
+ module FactoryHelpers
2
+ class CreateCollection < ActiveRecord::Migration
3
+ def self.up
4
+ create_table :collections do |t|
5
+ t.string :name
6
+ end
7
+ end
8
+
9
+ def self.down
10
+ drop_table :collections
11
+ end
12
+ end
13
+ end
14
+
15
+ module MigrationFactory
16
+
17
+ # Creates a new anonymous migration and puts something into self.up
18
+ # Example:
19
+ # migration = create_migration do
20
+ # create_table :items do |t|
21
+ # t.string :name
22
+ # end
23
+ # end
24
+ def create_migration(&blk)
25
+ migration = Class.new(ActiveRecord::Migration)
26
+
27
+ # This is the equivalent of
28
+ # class Foo
29
+ # def self.up
30
+ # end
31
+ # end
32
+ migration.metaclass.class_eval do
33
+ define_method(:up, &blk)
34
+ end
35
+ migration
36
+ end
37
+
38
+ # Creates a new, anonymous table migration and activates it
39
+ # Example:
40
+ # migration = create_table do |t|
41
+ # t.string :name
42
+ # end
43
+ def create_table(table = :items, opts = {}, &blk)
44
+ migration = create_migration do
45
+ create_table(table, opts, &blk)
46
+ end
47
+ migration.up
48
+ end
49
+
50
+ end
51
+
52
+ module ForeignKeyDefinitionFactory
53
+ def valid_foreign_key_definition(opt = {})
54
+ options = {
55
+ :from_table => 'items',
56
+ :to_table => 'collections'
57
+ }.merge(opt)
58
+ end
59
+
60
+ def valid_foreign_key_args(definition)
61
+ from_table = definition.delete(:from_table)
62
+ to_table = definition.delete(:to_table)
63
+ [from_table, to_table, definition]
64
+ end
65
+
66
+ def new_foreign_key(opt = {})
67
+ args = valid_foreign_key_args(valid_foreign_key_definition(opt))
68
+ Foreigner::ConnectionAdapters::ForeignKeyDefinition.new(*args)
69
+ end
70
+ end
@@ -0,0 +1,143 @@
1
+ require File.expand_path('../spec_helper.rb', File.dirname(__FILE__))
2
+
3
+ describe Foreigner::ConnectionAdapters::MysqlAdapter do
4
+ include MigrationFactory
5
+
6
+ before(:each) do
7
+ @adapter = AdapterHelper::MySQLTestAdapter.new
8
+ @adapter.recreate_test_environment
9
+ end
10
+
11
+ describe 'when extracting foreign keys from a table' do
12
+ it 'should extract single foreign key' do
13
+ create_table :items do |t|
14
+ t.string :name
15
+ t.references :collection, :null => false
16
+ t.foreign_key :collection
17
+ end
18
+
19
+ @adapter.foreign_keys(:items).length.should eql(1)
20
+ foreign_key = @adapter.foreign_keys(:items)[0]
21
+
22
+ # Duck Typing
23
+ foreign_key.should be_respond_to(:from_table)
24
+ foreign_key.should be_respond_to(:to_table)
25
+ foreign_key.should be_respond_to(:options)
26
+ end
27
+
28
+ it 'should extract multiple foreign keys' do
29
+ create_table :owners do |t|
30
+ t.string :name
31
+ end
32
+
33
+ create_table :items do |t|
34
+ t.string :name
35
+ t.references :collection, :null => false
36
+ t.foreign_key :collection
37
+ t.references :owner, :null => false
38
+ t.foreign_key :owner
39
+ end
40
+
41
+ @adapter.foreign_keys(:items).length.should eql(2)
42
+ foreign_key_names = @adapter.foreign_keys(:items).map(&:to_table)
43
+ foreign_key_names.should be_include('collections')
44
+ foreign_key_names.should be_include('owners')
45
+ end
46
+
47
+ it 'should extract referencing table' do
48
+ create_table :items do |t|
49
+ t.string :name
50
+ t.references :collection, :null => false
51
+ t.foreign_key :collection
52
+ end
53
+
54
+ @adapter.foreign_keys(:items).length.should eql(1)
55
+ foreign_key = @adapter.foreign_keys(:items)[0]
56
+ foreign_key.from_table.should eql('items')
57
+ end
58
+
59
+ it 'should extract foreign table' do
60
+ create_table :items do |t|
61
+ t.string :name
62
+ t.references :collection, :null => false
63
+ t.foreign_key :collection
64
+ end
65
+
66
+ @adapter.foreign_keys(:items).length.should eql(1)
67
+ foreign_key = @adapter.foreign_keys(:items)[0]
68
+ foreign_key.to_table.should eql('collections')
69
+ end
70
+
71
+ it 'should extract foreign key name' do
72
+ fk_name = 'custom_foreign_key'
73
+
74
+ create_migration do
75
+ create_table :items do |t|
76
+ t.string :name
77
+ t.references :collection, :null => false
78
+ end
79
+ add_foreign_key :items, :collections, :name => fk_name
80
+ end.up
81
+
82
+ @adapter.foreign_keys(:items).length.should eql(1)
83
+ foreign_key = @adapter.foreign_keys(:items)[0]
84
+ foreign_key.options[:name].should eql(fk_name)
85
+ end
86
+
87
+ it 'should extract foreign column' do
88
+ create_table :items do |t|
89
+ t.string :name
90
+ t.references :collection, :null => false
91
+ t.foreign_key :collection
92
+ end
93
+
94
+ @adapter.foreign_keys(:items).length.should eql(1)
95
+ foreign_key = @adapter.foreign_keys(:items)[0]
96
+ foreign_key.options[:column].should eql('collection_id')
97
+ end
98
+
99
+ it 'should extract primary key' do
100
+ primary_key = 'acctno'
101
+ create_table :accounts, :primary_key => primary_key do |t|
102
+ t.integer primary_key, :null => false
103
+ t.string :name
104
+ end
105
+
106
+ create_migration do
107
+ create_table :items do |t|
108
+ t.string :name
109
+ t.integer primary_key, :null => false
110
+ end
111
+ add_foreign_key :items, :accounts, :column => primary_key, :primary_key => primary_key
112
+ end.up
113
+
114
+ @adapter.foreign_keys(:items).length.should eql(1)
115
+ foreign_key = @adapter.foreign_keys(:items)[0]
116
+ foreign_key.options[:primary_key].should eql(primary_key)
117
+ end
118
+
119
+ it 'should extract :dependent => :nullify' do
120
+ @dependent = :nullify
121
+ create_table :items do |t|
122
+ t.string :name
123
+ t.references :collection, :foreign_key => {:dependent => @dependent}
124
+ end
125
+
126
+ foreign_key = @adapter.foreign_keys(:items)[0]
127
+ foreign_key.options[:dependent].should eql(@dependent)
128
+ end
129
+
130
+ it 'should extract :dependent => :delete' do
131
+ @dependent = :delete
132
+ create_table :items do |t|
133
+ t.string :name
134
+ t.references :collection, :foreign_key => {:dependent => @dependent}
135
+ end
136
+
137
+ foreign_key = @adapter.foreign_keys(:items)[0]
138
+ foreign_key.options[:dependent].should eql(@dependent)
139
+ end
140
+ end
141
+
142
+ end
143
+