sparkfly-foreigner 0.5.4

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