schema_plus 0.1.0.pre1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +25 -0
- data/Gemfile +3 -0
- data/MIT-LICENSE +25 -0
- data/README.rdoc +147 -0
- data/Rakefile +70 -0
- data/init.rb +1 -0
- data/lib/schema_plus/active_record/associations.rb +211 -0
- data/lib/schema_plus/active_record/base.rb +81 -0
- data/lib/schema_plus/active_record/connection_adapters/abstract_adapter.rb +96 -0
- data/lib/schema_plus/active_record/connection_adapters/column.rb +55 -0
- data/lib/schema_plus/active_record/connection_adapters/foreign_key_definition.rb +115 -0
- data/lib/schema_plus/active_record/connection_adapters/index_definition.rb +51 -0
- data/lib/schema_plus/active_record/connection_adapters/mysql_adapter.rb +111 -0
- data/lib/schema_plus/active_record/connection_adapters/postgresql_adapter.rb +163 -0
- data/lib/schema_plus/active_record/connection_adapters/schema_statements.rb +39 -0
- data/lib/schema_plus/active_record/connection_adapters/sqlite3_adapter.rb +78 -0
- data/lib/schema_plus/active_record/connection_adapters/table_definition.rb +130 -0
- data/lib/schema_plus/active_record/migration.rb +220 -0
- data/lib/schema_plus/active_record/schema.rb +27 -0
- data/lib/schema_plus/active_record/schema_dumper.rb +122 -0
- data/lib/schema_plus/active_record/validations.rb +139 -0
- data/lib/schema_plus/railtie.rb +12 -0
- data/lib/schema_plus/version.rb +3 -0
- data/lib/schema_plus.rb +248 -0
- data/schema_plus.gemspec +37 -0
- data/schema_plus.gemspec.rails3.0 +36 -0
- data/schema_plus.gemspec.rails3.1 +36 -0
- data/spec/association_spec.rb +529 -0
- data/spec/connections/mysql/connection.rb +18 -0
- data/spec/connections/mysql2/connection.rb +18 -0
- data/spec/connections/postgresql/connection.rb +15 -0
- data/spec/connections/sqlite3/connection.rb +14 -0
- data/spec/foreign_key_definition_spec.rb +23 -0
- data/spec/foreign_key_spec.rb +142 -0
- data/spec/index_definition_spec.rb +139 -0
- data/spec/index_spec.rb +71 -0
- data/spec/migration_spec.rb +405 -0
- data/spec/models/comment.rb +2 -0
- data/spec/models/post.rb +2 -0
- data/spec/models/user.rb +2 -0
- data/spec/references_spec.rb +78 -0
- data/spec/schema/auto_schema.rb +23 -0
- data/spec/schema/core_schema.rb +21 -0
- data/spec/schema_dumper_spec.rb +167 -0
- data/spec/schema_spec.rb +71 -0
- data/spec/spec_helper.rb +59 -0
- data/spec/support/extensions/active_model.rb +13 -0
- data/spec/support/helpers.rb +16 -0
- data/spec/support/matchers/automatic_foreign_key_matchers.rb +2 -0
- data/spec/support/matchers/have_index.rb +52 -0
- data/spec/support/matchers/reference.rb +66 -0
- data/spec/support/reference.rb +66 -0
- data/spec/validations_spec.rb +294 -0
- data/spec/views_spec.rb +140 -0
- metadata +269 -0
@@ -0,0 +1,163 @@
|
|
1
|
+
module SchemaPlus
|
2
|
+
module ActiveRecord
|
3
|
+
module ConnectionAdapters
|
4
|
+
# The Postgresql adapter implements the SchemaPlus extensions and
|
5
|
+
# enhancements
|
6
|
+
module PostgresqlAdapter
|
7
|
+
|
8
|
+
def self.included(base) #:nodoc:
|
9
|
+
base.class_eval do
|
10
|
+
remove_method :indexes
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# SchemaPlus provides the following extra options for Postgres
|
15
|
+
# indexes:
|
16
|
+
# * +:conditions+ - SQL conditions for the WHERE clause of the index
|
17
|
+
# * +:expression+ - SQL expression to index. column_name can be nil or ommitted, in which case :name must be provided
|
18
|
+
# * +:kind+ - index method for Postgresql to use
|
19
|
+
# * +:case_sensitive - if +false+ then the index will be created on LOWER(column_name)
|
20
|
+
#
|
21
|
+
# The <tt>:case_sensitive => false</tt> option ties in with Rails built-in support for case-insensitive searching:
|
22
|
+
# validates_uniqueness_of :name, :case_sensitive => false
|
23
|
+
#
|
24
|
+
def add_index(table_name, column_name, options = {})
|
25
|
+
column_name, options = [], column_name if column_name.is_a?(Hash)
|
26
|
+
column_names = Array(column_name).compact
|
27
|
+
if column_names.empty?
|
28
|
+
raise ArgumentError, "No columns and :expression missing from options - cannot create index" if options[:expression].blank?
|
29
|
+
raise ArgumentError, "Index name not given. Pass :name option" if options[:name].blank?
|
30
|
+
end
|
31
|
+
|
32
|
+
index_type = options[:unique] ? "UNIQUE" : ""
|
33
|
+
index_name = options[:name] || index_name(table_name, column_names)
|
34
|
+
conditions = options[:conditions]
|
35
|
+
|
36
|
+
if options[:expression] then
|
37
|
+
sql = "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{options[:expression]}"
|
38
|
+
else
|
39
|
+
quoted_column_names = column_names.map { |e| options[:case_sensitive] == false && e.to_s !~ /_id$/ ? "LOWER(#{quote_column_name(e)})" : quote_column_name(e) }
|
40
|
+
|
41
|
+
sql = "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{quoted_column_names.join(", ")})"
|
42
|
+
sql += " WHERE (#{ ::ActiveRecord::Base.send(:sanitize_sql, conditions, quote_table_name(table_name)) })" if conditions
|
43
|
+
end
|
44
|
+
execute sql
|
45
|
+
end
|
46
|
+
|
47
|
+
def supports_partial_indexes? #:nodoc:
|
48
|
+
true
|
49
|
+
end
|
50
|
+
|
51
|
+
def indexes(table_name, name = nil) #:nodoc:
|
52
|
+
schemas = schema_search_path.split(/,/).map { |p| quote(p) }.join(',')
|
53
|
+
result = query(<<-SQL, name)
|
54
|
+
SELECT distinct i.relname, d.indisunique, d.indkey, m.amname, t.oid,
|
55
|
+
pg_get_expr(d.indpred, t.oid), pg_get_expr(d.indexprs, t.oid)
|
56
|
+
FROM pg_class t, pg_class i, pg_index d, pg_am m
|
57
|
+
WHERE i.relkind = 'i'
|
58
|
+
AND i.relam = m.oid
|
59
|
+
AND d.indexrelid = i.oid
|
60
|
+
AND d.indisprimary = 'f'
|
61
|
+
AND t.oid = d.indrelid
|
62
|
+
AND t.relname = '#{table_name}'
|
63
|
+
AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname IN (#{schemas}) )
|
64
|
+
ORDER BY i.relname
|
65
|
+
SQL
|
66
|
+
|
67
|
+
result.map do |(index_name, is_unique, indkey, kind, oid, conditions, expression)|
|
68
|
+
unique = (is_unique == 't')
|
69
|
+
index_keys = indkey.split(" ")
|
70
|
+
|
71
|
+
columns = Hash[query(<<-SQL, "Columns for index #{index_name} on #{table_name}")]
|
72
|
+
SELECT a.attnum, a.attname
|
73
|
+
FROM pg_attribute a
|
74
|
+
WHERE a.attrelid = #{oid}
|
75
|
+
AND a.attnum IN (#{index_keys.join(",")})
|
76
|
+
SQL
|
77
|
+
|
78
|
+
column_names = columns.values_at(*index_keys).compact
|
79
|
+
if md = expression.try(:match, /^lower\(\(?([^)]+)\)?(::text)?\)$/i)
|
80
|
+
column_names << md[1]
|
81
|
+
end
|
82
|
+
::ActiveRecord::ConnectionAdapters::IndexDefinition.new(table_name, column_names,
|
83
|
+
:name => index_name,
|
84
|
+
:unique => unique,
|
85
|
+
:conditions => conditions,
|
86
|
+
:case_sensitive => !(expression =~ /lower/i),
|
87
|
+
:kind => kind.downcase == "btree" ? nil : kind,
|
88
|
+
:expression => expression)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def foreign_keys(table_name, name = nil) #:nodoc:
|
93
|
+
load_foreign_keys(<<-SQL, name)
|
94
|
+
SELECT f.conname, pg_get_constraintdef(f.oid), t.relname
|
95
|
+
FROM pg_class t, pg_constraint f
|
96
|
+
WHERE f.conrelid = t.oid
|
97
|
+
AND f.contype = 'f'
|
98
|
+
AND t.relname = '#{table_name}'
|
99
|
+
SQL
|
100
|
+
end
|
101
|
+
|
102
|
+
def reverse_foreign_keys(table_name, name = nil) #:nodoc:
|
103
|
+
load_foreign_keys(<<-SQL, name)
|
104
|
+
SELECT f.conname, pg_get_constraintdef(f.oid), t2.relname
|
105
|
+
FROM pg_class t, pg_class t2, pg_constraint f
|
106
|
+
WHERE f.confrelid = t.oid
|
107
|
+
AND f.conrelid = t2.oid
|
108
|
+
AND f.contype = 'f'
|
109
|
+
AND t.relname = '#{table_name}'
|
110
|
+
SQL
|
111
|
+
end
|
112
|
+
|
113
|
+
def views(name = nil) #:nodoc:
|
114
|
+
schemas = schema_search_path.split(/,/).map { |p| quote(p) }.join(',')
|
115
|
+
query(<<-SQL, name).map { |row| row[0] }
|
116
|
+
SELECT viewname
|
117
|
+
FROM pg_views
|
118
|
+
WHERE schemaname IN (#{schemas})
|
119
|
+
SQL
|
120
|
+
end
|
121
|
+
|
122
|
+
def view_definition(view_name, name = nil) #:nodoc:
|
123
|
+
result = query(<<-SQL, name)
|
124
|
+
SELECT pg_get_viewdef(oid)
|
125
|
+
FROM pg_class
|
126
|
+
WHERE relkind = 'v'
|
127
|
+
AND relname = '#{view_name}'
|
128
|
+
SQL
|
129
|
+
row = result.first
|
130
|
+
row.first.chomp(';') unless row.nil?
|
131
|
+
end
|
132
|
+
|
133
|
+
private
|
134
|
+
|
135
|
+
def load_foreign_keys(sql, name = nil) #:nodoc:
|
136
|
+
foreign_keys = []
|
137
|
+
|
138
|
+
query(sql, name).each do |row|
|
139
|
+
if row[1] =~ /^FOREIGN KEY \((.+?)\) REFERENCES (.+?)\((.+?)\)( ON UPDATE (.+?))?( ON DELETE (.+?))?( (DEFERRABLE|NOT DEFERRABLE))?$/
|
140
|
+
name = row[0]
|
141
|
+
from_table_name = row[2]
|
142
|
+
column_names = $1
|
143
|
+
references_table_name = $2
|
144
|
+
references_column_names = $3
|
145
|
+
on_update = $5
|
146
|
+
on_delete = $7
|
147
|
+
deferrable = $9 == "DEFERRABLE"
|
148
|
+
on_update = on_update ? on_update.downcase.gsub(' ', '_').to_sym : :no_action
|
149
|
+
on_delete = on_delete ? on_delete.downcase.gsub(' ', '_').to_sym : :no_action
|
150
|
+
|
151
|
+
foreign_keys << ForeignKeyDefinition.new(name,
|
152
|
+
from_table_name, column_names.split(', '),
|
153
|
+
references_table_name.sub(/^"(.*)"$/, '\1'), references_column_names.split(', '),
|
154
|
+
on_update, on_delete, deferrable)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
foreign_keys
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module SchemaPlus::ActiveRecord::ConnectionAdapters
|
2
|
+
module SchemaStatements
|
3
|
+
|
4
|
+
def self.included(base) #:nodoc:
|
5
|
+
base.class_eval do
|
6
|
+
alias_method_chain :create_table, :schema_plus
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
##
|
11
|
+
# :method: create_table
|
12
|
+
#
|
13
|
+
# SchemaPlus extends SchemaStatements::create_table to allow you to specify configuration options per table. Pass them in as a hash keyed by configuration set (see SchemaPlus::Config),
|
14
|
+
# for example:
|
15
|
+
#
|
16
|
+
# create_table :widgets, :foreign_keys => {:auto_create => true, :on_delete => :cascade} do |t|
|
17
|
+
# ...
|
18
|
+
# end
|
19
|
+
def create_table_with_schema_plus(table, options = {})
|
20
|
+
options = options.dup
|
21
|
+
config_options = {}
|
22
|
+
options.keys.each { |key| config_options[key] = options.delete(key) if SchemaPlus.config.class.attributes.include? key }
|
23
|
+
|
24
|
+
indexes = []
|
25
|
+
create_table_without_schema_plus(table, options) do |table_definition|
|
26
|
+
table_definition.schema_plus_config = SchemaPlus.config.merge(config_options)
|
27
|
+
table_definition.name = table
|
28
|
+
yield table_definition if block_given?
|
29
|
+
indexes = table_definition.indexes
|
30
|
+
end
|
31
|
+
indexes.each do |index|
|
32
|
+
add_index(table, index.columns, index.opts)
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module SchemaPlus
|
2
|
+
module ActiveRecord
|
3
|
+
module ConnectionAdapters
|
4
|
+
# SchemaPlus includes an Sqlite3 implementation of the AbstractAdapater
|
5
|
+
# extensions.
|
6
|
+
module Sqlite3Adapter
|
7
|
+
|
8
|
+
# :enddoc:
|
9
|
+
|
10
|
+
def add_foreign_key(table_name, column_names, references_table_name, references_column_names, options = {})
|
11
|
+
raise NotImplementedError, "Sqlite3 does not support altering a table to add foreign key constraints (table #{table_name.inspect} column #{column_names.inspect})"
|
12
|
+
end
|
13
|
+
|
14
|
+
def remove_foreign_key(table_name, foreign_key_name)
|
15
|
+
raise NotImplementedError, "Sqlite3 does not support altering a table to remove foreign key constraints (table #{table_name.inspect} constraint #{foreign_key_name.inspect})"
|
16
|
+
end
|
17
|
+
|
18
|
+
def foreign_keys(table_name, name = nil)
|
19
|
+
get_foreign_keys(table_name, name)
|
20
|
+
end
|
21
|
+
|
22
|
+
def reverse_foreign_keys(table_name, name = nil)
|
23
|
+
get_foreign_keys(nil, name).select{|definition| definition.references_table_name == table_name}
|
24
|
+
end
|
25
|
+
|
26
|
+
def views(name = nil)
|
27
|
+
execute("SELECT name FROM sqlite_master WHERE type='view'", name).collect{|row| row["name"]}
|
28
|
+
end
|
29
|
+
|
30
|
+
def view_definition(view_name, name = nil)
|
31
|
+
sql = execute("SELECT sql FROM sqlite_master WHERE type='view' AND name=#{quote(view_name)}", name).collect{|row| row["sql"]}.first
|
32
|
+
sql.sub(/^CREATE VIEW \S* AS\s+/im, '') unless sql.nil?
|
33
|
+
end
|
34
|
+
|
35
|
+
protected
|
36
|
+
|
37
|
+
def post_initialize
|
38
|
+
execute('PRAGMA FOREIGN_KEYS = 1')
|
39
|
+
end
|
40
|
+
|
41
|
+
def get_foreign_keys(table_name = nil, name = nil)
|
42
|
+
results = execute(<<-SQL, name)
|
43
|
+
SELECT name, sql FROM sqlite_master
|
44
|
+
WHERE type='table' #{table_name && %" AND name='#{table_name}' "}
|
45
|
+
SQL
|
46
|
+
|
47
|
+
re = %r[
|
48
|
+
\bFOREIGN\s+KEY\s* \(\s*[`"](.+?)[`"]\s*\)
|
49
|
+
\s*REFERENCES\s*[`"](.+?)[`"]\s*\((.+?)\)
|
50
|
+
(\s+ON\s+UPDATE\s+(.+?))?
|
51
|
+
(\s*ON\s+DELETE\s+(.+?))?
|
52
|
+
\s*[,)]
|
53
|
+
]x
|
54
|
+
|
55
|
+
foreign_keys = []
|
56
|
+
results.each do |row|
|
57
|
+
table_name = row["name"]
|
58
|
+
row["sql"].scan(re).each do |column_names, references_table_name, references_column_names, d1, on_update, d2, on_delete|
|
59
|
+
column_names = column_names.gsub('`', '').split(', ')
|
60
|
+
|
61
|
+
references_column_names = references_column_names.gsub('`"', '').split(', ')
|
62
|
+
on_update = on_update ? on_update.downcase.gsub(' ', '_').to_sym : :no_action
|
63
|
+
on_delete = on_delete ? on_delete.downcase.gsub(' ', '_').to_sym : :no_action
|
64
|
+
foreign_keys << ForeignKeyDefinition.new(nil,
|
65
|
+
table_name, column_names,
|
66
|
+
references_table_name, references_column_names,
|
67
|
+
on_update, on_delete)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
foreign_keys
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
module SchemaPlus::ActiveRecord::ConnectionAdapters
|
2
|
+
|
3
|
+
#
|
4
|
+
# SchemaPlus adds several methods to TableDefinition, allowing indexes
|
5
|
+
# and foreign key constraints to be defined within a
|
6
|
+
# <tt>create_table</tt> block of a migration, allowing for better
|
7
|
+
# encapsulation and more DRY definitions.
|
8
|
+
#
|
9
|
+
# For example, without SchemaPlus you might define a table like this:
|
10
|
+
#
|
11
|
+
# create_table :widgets do |t|
|
12
|
+
# t.string :name
|
13
|
+
# end
|
14
|
+
# add_index :widgets, :name
|
15
|
+
#
|
16
|
+
# But with SchemaPlus, the index can be defined within the create_table
|
17
|
+
# block, so you don't need to repeat the table name:
|
18
|
+
#
|
19
|
+
# create_table :widgets do |t|
|
20
|
+
# t.string :name
|
21
|
+
# t.index :name
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# Even more DRY, you can define the index as part of the column
|
25
|
+
# definition, via:
|
26
|
+
#
|
27
|
+
# create_table :widgets do |t|
|
28
|
+
# t.string :name, :index => true
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# For details about the :index option (including unique and multi-column indexes), see the
|
32
|
+
# documentation for Migration::ClassMethods#add_column
|
33
|
+
#
|
34
|
+
# SchemaPlus also supports creation of foreign key constraints analogously, using Migration::ClassMethods#add_foreign_key or TableDefinition#foreign_key or as part of the column definition, for example:
|
35
|
+
#
|
36
|
+
# create_table :posts do |t| # not DRY
|
37
|
+
# t.integer :author_id
|
38
|
+
# end
|
39
|
+
# add_foreign_key :posts, :author_id, :references => :authors
|
40
|
+
#
|
41
|
+
# create_table :posts do |t| # DRYer
|
42
|
+
# t.integer :author_id
|
43
|
+
# t.foreign_key :author_id, :references => :authors
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
# create_table :posts do |t| # Dryest
|
47
|
+
# t.integer :author_id, :references => :authors
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# <b>NOTE:</b> In the standard configuration, SchemaPlus automatically
|
51
|
+
# creates foreign key constraints for columns whose names end in
|
52
|
+
# <tt>_id</tt>. So the above examples are redundant, unless automatic
|
53
|
+
# creation was disabled at initialization in the global Config.
|
54
|
+
#
|
55
|
+
# Finally, the configuration for foreign keys can be overriden on a per-table
|
56
|
+
# basis by passing Config options to Migration::ClassMethods#create_table, such as
|
57
|
+
#
|
58
|
+
# create_table :students, :foreign_keys => {:auto_create => false} do
|
59
|
+
# t.integer :student_id
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
module TableDefinition
|
63
|
+
|
64
|
+
attr_accessor :schema_plus_config #:nodoc:
|
65
|
+
|
66
|
+
def self.included(base) #:nodoc:
|
67
|
+
base.class_eval do
|
68
|
+
attr_accessor :name
|
69
|
+
attr_accessor :indexes
|
70
|
+
alias_method_chain :initialize, :schema_plus
|
71
|
+
alias_method_chain :column, :schema_plus
|
72
|
+
alias_method_chain :primary_key, :schema_plus
|
73
|
+
alias_method_chain :to_sql, :schema_plus
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def initialize_with_schema_plus(*args) #:nodoc:
|
78
|
+
initialize_without_schema_plus(*args)
|
79
|
+
@foreign_keys = []
|
80
|
+
@indexes = []
|
81
|
+
end
|
82
|
+
|
83
|
+
def primary_key_with_schema_plus(name, options = {}) #:nodoc:
|
84
|
+
column(name, :primary_key, options)
|
85
|
+
end
|
86
|
+
|
87
|
+
def column_with_schema_plus(name, type, options = {}) #:nodoc:
|
88
|
+
column_without_schema_plus(name, type, options)
|
89
|
+
if references = ActiveRecord::Migration.get_references(self.name, name, options, schema_plus_config)
|
90
|
+
if index = options.fetch(:index, fk_use_auto_index?)
|
91
|
+
self.column_index(name, index)
|
92
|
+
end
|
93
|
+
foreign_key(name, references.first, references.last,
|
94
|
+
options.reverse_merge(:on_update => schema_plus_config.foreign_keys.on_update,
|
95
|
+
:on_delete => schema_plus_config.foreign_keys.on_delete))
|
96
|
+
elsif options[:index]
|
97
|
+
self.column_index(name, options[:index])
|
98
|
+
end
|
99
|
+
self
|
100
|
+
end
|
101
|
+
|
102
|
+
def to_sql_with_schema_plus #:nodoc:
|
103
|
+
sql = to_sql_without_schema_plus
|
104
|
+
sql << ', ' << @foreign_keys.map(&:to_sql) * ', ' unless @foreign_keys.empty?
|
105
|
+
sql
|
106
|
+
end
|
107
|
+
|
108
|
+
# Define an index for the current
|
109
|
+
def index(column_name, options={})
|
110
|
+
@indexes << ::ActiveRecord::ConnectionAdapters::IndexDefinition.new(self.name, column_name, options)
|
111
|
+
end
|
112
|
+
|
113
|
+
def foreign_key(column_names, references_table_name, references_column_names, options = {})
|
114
|
+
@foreign_keys << ForeignKeyDefinition.new(options[:name], nil, column_names, ::ActiveRecord::Migrator.proper_table_name(references_table_name), references_column_names, options[:on_update], options[:on_delete], options[:deferrable])
|
115
|
+
self
|
116
|
+
end
|
117
|
+
|
118
|
+
protected
|
119
|
+
def column_index(name, options) #:nodoc:
|
120
|
+
options = {} if options == true
|
121
|
+
name = [name] + Array.wrap(options.delete(:with)).compact
|
122
|
+
self.index(name, options)
|
123
|
+
end
|
124
|
+
|
125
|
+
def fk_use_auto_index? #:nodoc:
|
126
|
+
schema_plus_config.foreign_keys.auto_index? && !ActiveRecord::Schema.defining?
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,220 @@
|
|
1
|
+
module SchemaPlus::ActiveRecord
|
2
|
+
# SchemaPlus extends ActiveRecord::Migration with several enhancements. See documentation at Migration::ClassMethods
|
3
|
+
module Migration
|
4
|
+
def self.included(base) #:nodoc:
|
5
|
+
base.extend(ClassMethods)
|
6
|
+
end
|
7
|
+
|
8
|
+
#
|
9
|
+
# SchemaPlus extends ActiveRecord::Migration with the following enhancements.
|
10
|
+
#
|
11
|
+
module ClassMethods
|
12
|
+
|
13
|
+
# Create a new view, given its name and SQL definition
|
14
|
+
#
|
15
|
+
def create_view(view_name, definition)
|
16
|
+
connection.create_view(view_name, definition)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Drop the named view
|
20
|
+
def drop_view(view_name)
|
21
|
+
connection.drop_view(view_name)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Define a foreign key constraint. Valid options are :on_update,
|
25
|
+
# :on_delete, and :deferrable, with values as described at
|
26
|
+
# ConnectionAdapters::ForeignKeyDefinition
|
27
|
+
#
|
28
|
+
# (NOTE: Sqlite3 does not support altering a table to add foreign-key
|
29
|
+
# constraints; they must be included in the table specification when
|
30
|
+
# it's created. If you're using Sqlite3, this method will raise an
|
31
|
+
# error.)
|
32
|
+
def add_foreign_key(table_name, column_names, references_table_name, references_column_names, options = {})
|
33
|
+
connection.add_foreign_key(table_name, column_names, references_table_name, references_column_names, options)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Remove a foreign key constraint
|
37
|
+
#
|
38
|
+
# (NOTE: Sqlite3 does not support altering a table to remove
|
39
|
+
# foreign-key constraints. If you're using Sqlite3, this method will
|
40
|
+
# raise an error.)
|
41
|
+
def remove_foreign_key(table_name, foreign_key_name)
|
42
|
+
connection.remove_foreign_key(table_name, foreign_key_name)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Enhances ActiveRecord::Migration#add_column to support indexes and foreign keys, with automatic creation
|
46
|
+
#
|
47
|
+
# == Indexes
|
48
|
+
#
|
49
|
+
# The <tt>:index</tt> option takes a hash of parameters to pass to ActiveRecord::Migration.add_index. Thus
|
50
|
+
#
|
51
|
+
# add_column('books', 'isbn', :string, :index => {:name => "ISBN-index", :unique => true })
|
52
|
+
#
|
53
|
+
# is equivalent to:
|
54
|
+
#
|
55
|
+
# add_column('books', 'isbn', :string)
|
56
|
+
# add_index('books', ['isbn'], :name => "ISBN-index", :unique => true)
|
57
|
+
#
|
58
|
+
#
|
59
|
+
# 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
|
60
|
+
#
|
61
|
+
# add_column('contacts', 'phone_number', :string, :index => { :with => [:country_code, :area_code], :unique => true })
|
62
|
+
#
|
63
|
+
# is equivalent to:
|
64
|
+
#
|
65
|
+
# add_column('contacts', 'phone_number', :string)
|
66
|
+
# add_index('contacts', ['phone_number', 'country_code', 'area_code'], :unique => true)
|
67
|
+
#
|
68
|
+
#
|
69
|
+
# Some convenient shorthands are available:
|
70
|
+
#
|
71
|
+
# add_column('books', 'isbn', :index => true) # adds index with no extra options
|
72
|
+
# add_column('books', 'isbn', :index => :unique) # adds index with :unique => true
|
73
|
+
#
|
74
|
+
# == Foreign Key Constraints
|
75
|
+
#
|
76
|
+
# The +:references+ option takes the name of a table to reference in
|
77
|
+
# a foreign key constraint. For example:
|
78
|
+
#
|
79
|
+
# add_column('widgets', 'color', :integer, :references => 'colors')
|
80
|
+
#
|
81
|
+
# is equivalent to
|
82
|
+
#
|
83
|
+
# add_column('widgets', 'color', :integer)
|
84
|
+
# add_foreign_key('widgets', 'color', 'colors', 'id')
|
85
|
+
#
|
86
|
+
# The foreign column name defaults to +id+, but a different column
|
87
|
+
# can be specified using <tt>:references => [table_name,column_name]</tt>
|
88
|
+
#
|
89
|
+
# Additional options +:on_update+ and +:on_delete+ can be spcified,
|
90
|
+
# with values as described at ConnectionAdapters::ForeignKeyDefinition. For example:
|
91
|
+
#
|
92
|
+
# add_column('comments', 'post', :integer, :references => 'posts', :on_delete => :cascade)
|
93
|
+
#
|
94
|
+
# Global default values for +:on_update+ and +:on_delete+ can be
|
95
|
+
# specified in SchemaPlus.steup via, e.g., <tt>config.foreign_keys.on_update = :cascade</tt>
|
96
|
+
#
|
97
|
+
# == Automatic Foreign Key Constraints
|
98
|
+
#
|
99
|
+
# SchemaPlus supports the convention of naming foreign key columns
|
100
|
+
# with a suffix of +_id+. That is, if you define a column suffixed
|
101
|
+
# with +_id+, SchemaPlus assumes an implied :references to a table
|
102
|
+
# whose name is the column name prefix, pluralized. For example,
|
103
|
+
# these are equivalent:
|
104
|
+
#
|
105
|
+
# add_column('posts', 'author_id', :integer)
|
106
|
+
# add_column('posts', 'author_id', :integer, :references => 'authors')
|
107
|
+
#
|
108
|
+
# As a special case, if the column is named 'parent_id', SchemaPlus
|
109
|
+
# assumes it's a self reference, for a record that acts as a node of
|
110
|
+
# a tree. Thus, these are equivalent:
|
111
|
+
#
|
112
|
+
# add_column('sections', 'parent_id', :integer)
|
113
|
+
# add_column('sections', 'parent_id', :integer, :references => 'sections')
|
114
|
+
#
|
115
|
+
# If the implicit +:references+ value isn't what you want (e.g., the
|
116
|
+
# table name isn't pluralized), you can explicitly specify
|
117
|
+
# +:references+ and it will override the implicit value.
|
118
|
+
#
|
119
|
+
# If you don't want a foreign key constraint to be created, specify
|
120
|
+
# <tt>:references => nil</tt>.
|
121
|
+
# To disable automatic foreign key constraint creation globally, set
|
122
|
+
# <tt>config.foreign_keys.auto_create = false</tt> in
|
123
|
+
# SchemaPlus.steup.
|
124
|
+
#
|
125
|
+
# == Automatic Foreign Key Indexes
|
126
|
+
#
|
127
|
+
# Since efficient use of foreign key constraints requires that the
|
128
|
+
# referencing column be indexed, SchemaPlus will automatically create
|
129
|
+
# an index for the column if it created a foreign key. Thus
|
130
|
+
#
|
131
|
+
# add_column('widgets', 'color', :integer, :references => 'colors')
|
132
|
+
#
|
133
|
+
# is equivalent to:
|
134
|
+
#
|
135
|
+
# add_column('widgets', 'color', :integer, :references => 'colors', :index => true)
|
136
|
+
#
|
137
|
+
# If you want to pass options to the index, you can explcitly pass
|
138
|
+
# index options, such as <tt>:index => :unique</tt>.
|
139
|
+
#
|
140
|
+
# If you don't want an index to be created, specify
|
141
|
+
# <tt>:index => nil</tt>.
|
142
|
+
# To disable automatic foreign key index creation globally, set
|
143
|
+
# <tt>config.foreign_keys.auto_index = false</tt> in
|
144
|
+
# SchemaPlus.steup. (*Note*: If you're using MySQL, it will
|
145
|
+
# automatically create an index for foreign keys if you don't.)
|
146
|
+
#
|
147
|
+
def add_column(table_name, column_name, type, options = {})
|
148
|
+
super
|
149
|
+
handle_column_options(table_name, column_name, options)
|
150
|
+
end
|
151
|
+
|
152
|
+
# Enhances ActiveRecord::Migration#change_column to support indexes and foreign keys same as add_column.
|
153
|
+
def change_column(table_name, column_name, type, options = {})
|
154
|
+
super
|
155
|
+
remove_foreign_key_if_exists(table_name, column_name)
|
156
|
+
handle_column_options(table_name, column_name, options)
|
157
|
+
end
|
158
|
+
|
159
|
+
# Determines referenced table and column.
|
160
|
+
# Used in migrations.
|
161
|
+
#
|
162
|
+
# If auto_create is true:
|
163
|
+
# get_references('comments', 'post_id') # => ['posts', 'id']
|
164
|
+
#
|
165
|
+
# And if <tt>column_name</tt> is parent_id it references to the same table
|
166
|
+
# get_references('pages', 'parent_id') # => ['pages', 'id']
|
167
|
+
#
|
168
|
+
# If :references option is given, it is used (whether or not auto_create is true)
|
169
|
+
# get_references('widgets', 'main_page_id', :references => 'pages'))
|
170
|
+
# # => ['pages', 'id']
|
171
|
+
#
|
172
|
+
# Also the referenced id column may be specified:
|
173
|
+
# get_references('addresses', 'member_id', :references => ['users', 'uuid'])
|
174
|
+
# # => ['users', 'uuid']
|
175
|
+
def get_references(table_name, column_name, options = {}, config=nil) #:nodoc:
|
176
|
+
column_name = column_name.to_s
|
177
|
+
if options.has_key?(:references)
|
178
|
+
references = options[:references]
|
179
|
+
references = [references, :id] unless references.nil? || references.is_a?(Array)
|
180
|
+
references
|
181
|
+
elsif (config || SchemaPlus.config).foreign_keys.auto_create? && !ActiveRecord::Schema.defining?
|
182
|
+
if column_name == 'parent_id'
|
183
|
+
[table_name, :id]
|
184
|
+
elsif column_name =~ /^(.*)_id$/
|
185
|
+
determined_table_name = ActiveRecord::Base.pluralize_table_names ? $1.to_s.pluralize : $1
|
186
|
+
[determined_table_name, :id]
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
protected
|
192
|
+
def handle_column_options(table_name, column_name, options) #:nodoc:
|
193
|
+
if references = get_references(table_name, column_name, options)
|
194
|
+
if index = options.fetch(:index, SchemaPlus.config.foreign_keys.auto_index? && !ActiveRecord::Schema.defining?)
|
195
|
+
column_index(table_name, column_name, index)
|
196
|
+
end
|
197
|
+
add_foreign_key(table_name, column_name, references.first, references.last,
|
198
|
+
options.reverse_merge(:on_update => SchemaPlus.config.foreign_keys.on_update,
|
199
|
+
:on_delete => SchemaPlus.config.foreign_keys.on_delete))
|
200
|
+
elsif options[:index]
|
201
|
+
column_index(table_name, column_name, options[:index])
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def column_index(table_name, column_name, options) #:nodoc:
|
206
|
+
options = {} if options == true
|
207
|
+
options = { :unique => true } if options == :unique
|
208
|
+
column_name = [column_name] + Array.wrap(options.delete(:with)).compact
|
209
|
+
add_index(table_name, column_name, options)
|
210
|
+
end
|
211
|
+
|
212
|
+
def remove_foreign_key_if_exists(table_name, column_name) #:nodoc:
|
213
|
+
foreign_keys = ActiveRecord::Base.connection.foreign_keys(table_name.to_s)
|
214
|
+
fk = foreign_keys.detect { |fk| fk.table_name == table_name.to_s && fk.column_names == Array(column_name).collect(&:to_s) }
|
215
|
+
remove_foreign_key(table_name, fk.name) if fk
|
216
|
+
end
|
217
|
+
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|