schema_plus 0.1.0.pre1
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.
- 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
|