theaboutbox-foreigner 0.7.2

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/semantics/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,72 @@
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
+
33
+ # ActiveSupport 3.0 changed the name to singleton_class
34
+ (migration.respond_to?(:singleton_class) ? migration.singleton_class : migration.metaclass).class_eval do
35
+ define_method(:up, &blk)
36
+ end
37
+ migration
38
+ end
39
+
40
+ # Creates a new, anonymous table migration and activates it
41
+ # Example:
42
+ # migration = create_table do |t|
43
+ # t.string :name
44
+ # end
45
+ def create_table(table = :items, opts = {}, &blk)
46
+ migration = create_migration do
47
+ create_table(table, opts, &blk)
48
+ end
49
+ migration.up
50
+ end
51
+
52
+ end
53
+
54
+ module ForeignKeyDefinitionFactory
55
+ def valid_foreign_key_definition(opt = {})
56
+ options = {
57
+ :from_table => 'items',
58
+ :to_table => 'collections'
59
+ }.merge(opt)
60
+ end
61
+
62
+ def valid_foreign_key_args(definition)
63
+ from_table = definition.delete(:from_table)
64
+ to_table = definition.delete(:to_table)
65
+ [from_table, to_table, definition]
66
+ end
67
+
68
+ def new_foreign_key(opt = {})
69
+ args = valid_foreign_key_args(valid_foreign_key_definition(opt))
70
+ Foreigner::ConnectionAdapters::ForeignKeyDefinition.new(*args)
71
+ end
72
+ end