schema_plus 1.0.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,360 @@
1
+ # SchemaPlus
2
+
3
+ SchemaPlus is an ActiveRecord extension that provides enhanced capabilities
4
+ for schema definition and querying, including: enhanced and more DRY index
5
+ capabilities, support and automation for foreign key constraints, and support
6
+ for views.
7
+
8
+ For added rails DRYness see also the gems
9
+ [schema_associations](http://rubygems.org/gems/schema_associations) and
10
+ [schema_validations](http://rubygems.org/gems/schema_validations)
11
+
12
+ [<img
13
+ src="https://secure.travis-ci.org/lomba/schema_plus.png"/>](http://travis-ci.o
14
+ rg/lomba/schema_plus) [<img src="https://gemnasium.com/lomba/schema_plus.png"
15
+ alt="Dependency Status" />](https://gemnasium.com/lomba/schema_plus)
16
+
17
+ ## Compatibility
18
+
19
+ SchemaPlus supports all combinations of:
20
+ * rails 3.2
21
+ * PostgreSQL, MySQL (using mysql or mysql2 gem), or SQLite3 (using sqlite3
22
+ 3.7.7 which has foreign key support)
23
+ * MRI ruby 1.9.2 or 1.9.3
24
+
25
+
26
+ Note: As of version 1.0.0, SchemaPlus no longer supports rails 2.3, 3.0 and
27
+ 3.1 and ruby 1.8.7. The last version to support them was 0.4.1.
28
+
29
+ ## Installation
30
+
31
+ Install from http://rubygems.org via
32
+
33
+ $ gem install "schema_plus"
34
+
35
+ or in a Gemfile
36
+
37
+ gem "schema_plus"
38
+
39
+ ## Features
40
+
41
+ Here some examples that show off the high points. For full details see the
42
+ [RDoc documentation](http://rubydoc.info/gems/schema_plus).
43
+
44
+ ### Indexes
45
+
46
+ With standard rails migrations, you specify indexes separately from the table
47
+ definition:
48
+
49
+ # Standard Rails approach...
50
+ create_table :parts do |t|
51
+ t.string :name
52
+ t.string :product_code
53
+ end
54
+
55
+ add_index :parts, :name # index repeats table and column names and is defined separately
56
+ add_index :parts, :product_code, unique: true
57
+
58
+ But with SchemaPlus you can specify your indexes when you define each column,
59
+ with options as desired
60
+
61
+ # More DRY way...
62
+ create_table :parts do |t|
63
+ t.string :name, index: true
64
+ t.string :product_code, index: { unique: true }
65
+ end
66
+
67
+ The options hash can include an index name:
68
+
69
+ t.string :product_code, index: { unique: true, name: "my_index_name" }
70
+
71
+ You can also create multi-column indexes, for example:
72
+
73
+ t.string :first_name
74
+ t.string :last_name, index: { with: :first_name }
75
+
76
+ t.string :country_code
77
+ t.string :area_code
78
+ t.string :local_number index: { with: [:country_code, :area_code], unique: true }
79
+
80
+ And you can specify index orders:
81
+
82
+ t.string :first_name
83
+ t.string :last_name, index: { with: :first_name, order: { first_name: :desc, last_name: :asc}}
84
+
85
+ As a convenient shorthand, the :unique option can be specified as
86
+
87
+ t.string :product_code, index: :unique
88
+
89
+ which is equivalent to
90
+
91
+ t.string :product_code, index: { unique: true }
92
+
93
+ If you're using Postgresql, SchemaPlus provides support for conditions,
94
+ expressions, index methods, and case-insensitive indexes:
95
+
96
+ t.string :last_name, index: { conditions: 'deleted_at IS NULL' }
97
+ t.string :last_name, index: { expression: 'upper(last_name)' }
98
+ t.string :last_name, index: { kind: 'hash' }
99
+ t.string :last_name, index: { case_sensitive: false } # shorthand for expression: 'lower(last_name)'
100
+
101
+ These features are available also in ActiveRecord::Migration.add_index. See
102
+ doc at SchemaPlus::ActiveRecord::ConnectionAdapters::PostgresqlAdapter and
103
+ SchemaPlus::ActiveRecord::ConnectionAdapters::IndexDefinition
104
+
105
+ When you query column information using ActiveRecord::Base#columns, SchemaPlus
106
+ analogously provides index information relevant to each column: which indexes
107
+ reference the column, whether the column must be unique, etc. See doc at
108
+ SchemaPlus::ActiveRecord::ConnectionAdapters::Column
109
+
110
+ SchemaPlus also tidies some index-related behavior:
111
+
112
+ * Rails' various db adapters have inconsistent behavior regarding an attempt
113
+ to create a duplicate index: some quietly ignore the attempt, some raise
114
+ an error. SchemaPlus regularizes the behavor to ignore the attempt for
115
+ all db adapters.
116
+
117
+ * If you rename a table, indexes named using rails' automatic naming
118
+ convention will be renamed correspondingly.
119
+
120
+
121
+ ### Foreign Key Constraints
122
+
123
+ SchemaPlus adds support for foreign key constraints. In fact, for the common
124
+ convention that you name a column with suffix `_id` to indicate that it's a
125
+ foreign key, SchemaPlus automatically defines the appropriate constraint.
126
+
127
+ SchemaPlus also creates foreign key constraints for rails' `t.references` or
128
+ `t.belongs_to`, which take the singular of the referenced table name and
129
+ implicitly create the column suffixed with `_id`.
130
+
131
+ You can explicitly specify whether or not to generate a foreign key
132
+ constraint, and specify or override automatic options, using the
133
+ `:foreign_key` keyword
134
+
135
+ Here are some examples:
136
+
137
+ t.integer :author_id # automatically references table 'authors', key id
138
+ t.integer :parent_id # special name parent_id automatically references its own table (for tree nodes)
139
+ t.integer :author_id, foreign_key: true # same as default automatic behavior
140
+ t.integer :author, foreign_key: true # non-conventional column name needs to force creation, table name is assumed to be 'authors'
141
+ t.integer :author_id, foreign_key: false # don't create a constraint
142
+
143
+ t.integer :author_id, foreign_key: { references: :authors } # same as automatic behavior
144
+ t.integer :author, foreign_key: { reference: :authors} # same default name
145
+ t.integer :author_id, foreign_key: { references: [:authors, :id] } # same as automatic behavior
146
+ t.integer :author_id, foreign_key: { references: :people } # override table name
147
+ t.integer :author_id, foreign_key: { references: [:people, :ssn] } # override table name and key
148
+ t.integer :author_id, foreign_key: { references: nil } # don't create a constraint
149
+ t.integer :author_id, foreign_key: { name: "my_fk" } # override default generated constraint name
150
+ t.integer :author_id, foreign_key: { on_delete: :cascade }
151
+ t.integer :author_id, foreign_key: { on_update: :set_null }
152
+ t.integer :author_id, foreign_key: { deferrable: true }
153
+ t.integer :author_id, foreign_key: { deferrable: :initially_deferred }
154
+
155
+ Of course the options can be combined, e.g.
156
+
157
+ t.integer :author_id, foreign_key: { name: "my_fk", on_delete: :no_action }
158
+
159
+ As a shorthand, all options except `:name` can be specified without placing
160
+ them in a hash, e.g.
161
+
162
+ t.integer :author_id, on_delete: :cascade
163
+ t.integer :author_id, references: nil
164
+
165
+ The foreign key behavior can be configured globally (see Config) or per-table
166
+ (see create_table).
167
+
168
+ To examine your foreign key constraints, `connection.foreign_keys` returns a
169
+ list of foreign key constraints defined for a given table, and
170
+ `connection.reverse_foreign_keys` returns a list of foreign key constraints
171
+ that reference a given table. See
172
+ SchemaPlus::ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.
173
+
174
+ ### Tables
175
+
176
+ SchemaPlus extends rails' `drop_table` method to accept these options:
177
+
178
+ drop_table :table_name # same as rails
179
+ drop_table :table_name, if_exists: true # no error if table doesn't exist
180
+ drop_table :table_name, cascade: true # delete dependencies
181
+
182
+ The `:cascade` option is particularly useful given foreign key constraints.
183
+ For Postgresql it is implemented using `DROP TABLE...CASCADE` which deletes
184
+ all dependencies. For MySQL, SchemaPlus implements the `:cascade` option to
185
+ delete foreign key references, but does not delete any other dependencies.
186
+ For Sqlite3, the `:cascade` option is ignored, but Sqlite3 always drops tables
187
+ with cascade-like behavior.
188
+
189
+ SchemaPlus likewise extends `create_table ... force: true` to use `:cascade`
190
+
191
+ ### Views
192
+
193
+ SchemaPlus provides support for creating and dropping views. In a migration,
194
+ a view can be created using a rails relation or literal sql:
195
+
196
+ create_view :posts_commented_by_staff, Post.joins(comment: user).where(users: {role: 'staff'}).uniq
197
+ create_view :uncommented_posts, "SELECT * FROM posts LEFT OUTER JOIN comments ON comments.post_id = posts.id WHERE comments.id IS NULL"
198
+
199
+ And can be dropped:
200
+
201
+ drop_view :posts_commented_by_staff
202
+ drop_view :uncommented_posts
203
+
204
+ ActiveRecord works with views the same as with ordinary tables. That is, for
205
+ the above views you can define
206
+
207
+ class PostCommentedByStaff < ActiveRecord::Base
208
+ table_name = "posts_commented_by_staff"
209
+ end
210
+
211
+ class UncommentedPost < ActiveRecord::Base
212
+ end
213
+
214
+ ### Column Defaults: Expressions
215
+
216
+ SchemaPlus allows defaults to be set using expressions or constant values:
217
+
218
+ t.datetime :seen_at, default: { expr: 'NOW()' }
219
+ t.datetime :seen_at, default: { value: "2011-12-11 00:00:00" }
220
+
221
+ Note that in MySQL only the TIMESTAMP column data type accepts SQL column
222
+ defaults and Rails uses DATETIME, so expressions can't be used with MySQL.
223
+
224
+ The standard syntax will still work as usual:
225
+
226
+ t.datetime :seen_at, default: "2011-12-11 00:00:00"
227
+
228
+ Also, as a convenience
229
+
230
+ t.datetime :seen_at, default: :now
231
+
232
+ resolves to:
233
+
234
+ NOW() # PostgreSQL
235
+ (DATETIME('now')) # SQLite3
236
+ invalid # MySQL
237
+
238
+ ### Column Defaults: Using
239
+
240
+ SchemaPlus introduces a constant `ActiveRecord::DB_DEFAULT` that you can use
241
+ to explicitly instruct the database to use the column default value (or
242
+ expression). For example:
243
+
244
+ Post.create(category: ActiveRecord::DB_DEFAULT)
245
+ post.update_attributes(category: ActiveRecord::DB_DEFAULT)
246
+
247
+ (Without `ActiveRecord::DB_DEFAULT`, you can update a value to `NULL` but not
248
+ to its default value.)
249
+
250
+ Note that after updating, you would need to reload a record to replace
251
+ `ActiveRecord::DB_DEFAULT` with the value assigned by the database.
252
+
253
+ Note also that Sqlite3 does not support `ActiveRecord::DB_DEFAULT`; attempting
254
+ to use it will raise `ActiveRecord::StatementInvalid`
255
+
256
+ ### Schema Dump and Load (schema.rb)
257
+
258
+ When dumping `schema.rb`, SchemaPlus orders the views and tables in the schema
259
+ dump alphabetically, but subject to the requirement that each table or view be
260
+ defined before those that depend on it. This allows all foreign key
261
+ constraints to be defined within the scope of the table definition. (Unless
262
+ there are cyclical dependencies, in which case some foreign keys constraints
263
+ must be defined later.)
264
+
265
+ Also, when dumping `schema.rb`, SchemaPlus dumps explicit foreign key
266
+ definition statements rather than relying on the auto-creation behavior, for
267
+ maximum clarity and for independence from global config. And correspondingly,
268
+ when loading a schema, i.e. with the context of `ActiveRecord::Schema.define`,
269
+ SchemaPlus ensures that auto creation of foreign key constraints is turned off
270
+ regardless of the global setting. But if for some reason you are creating
271
+ your schema.rb file by hand, and would like to take advantage of auto-creation
272
+ of foreign key constraints, you can re-enable it:
273
+
274
+ ActiveRecord::Schema.define do
275
+ SchemaPlus.config.foreign_keys.auto_create = true
276
+ SchemaPlus.config.foreign_keys.auto_index = true
277
+
278
+ create_table ...etc...
279
+ end
280
+
281
+ ## Release notes:
282
+
283
+ ### 1.1.0
284
+
285
+ * Add support for drop_table :cascade => true. Note that until now,
286
+ :cascade was implicitly true. So this change might break existing code
287
+ that relied on the incorrect implicit cascade behavior.
288
+ * Add support for :deferrable => :initially_deferred (thanks to
289
+ [@bhavinkamani](https://github.com/bhavinkamani))
290
+ * Bug fix: Circular Reference/Stack Level Too Deep in Column#to_json.
291
+ Thanks to [@robdimarco](https://github.com/robdimarco) for tracking down the problem
292
+ * Bug fix: More robust handling of foreign keys with schema namespaces
293
+
294
+
295
+ ### 1.0.1
296
+
297
+ * README cleanups (thanks to [@denispeplin](https://github.com/denispeplin))
298
+ * Now raises ArgumentError if index has both :case_sensitive => false and an
299
+ :expression
300
+ * Now creates consistent default name for foreign key constraints
301
+ * Bug fix: respect :length keyword for index (thanks to [@teleological](https://github.com/teleological))
302
+ * Bug fix: renaming table with multiple foreign key constraints (thanks to
303
+ [@teleological](https://github.com/teleological))
304
+ * Bug fix: don't dump :case_sensitive => false for index with an expression
305
+ that includes "lower(name)".
306
+ * Bug fix: Properly dump multi-column case-insensitive indexes
307
+
308
+
309
+ ### 1.0.0
310
+
311
+ * No longer support rails < 3.2 and ruby < 1.9
312
+ * New feature: specify foreign key constraints using :foreign_key => { ...
313
+ }, motivated in particular to support :name (suggested by [@daniele-m](https://github.com/daniele-m))
314
+ * New feature: create view using ActiveRecord relation
315
+ * New feature: `ActiveRecord::DB_DEFAULT` (suggested by
316
+ [@zaadjis](https://github.com/zaadjis))
317
+ * New feature: renaming a table renames its indexes and constraints
318
+ correspondingly.
319
+ * Bug fix for postgres :kind index attribute (thanks to [@eugenebolshakov](https://github.com/eugenebolshakov))
320
+ * Sort fks in dump for stability (thanks to [@zephyr-dev](https://github.com/zephyr-dev))
321
+ * Bug fix: change_column should maintain foreign key constraints even when
322
+ config.foreign_keys.auto_create is false
323
+ * Bug fix: quote default expressions in schema dump (thanks to [@jonleighton](https://github.com/jonleighton))
324
+ * Bug fix: when removing a foreign key constraint, remove its auto-generated
325
+ index.
326
+ * Bug fix: SchemaDumper.ignore_tables needs to support regexps (suggested by
327
+ [@mtalcott](https://github.com/mtalcott))
328
+ * Bug fix: More robust handling of Postgresql schema_search path (suggested
329
+ by [@mtalcott](https://github.com/mtalcott))
330
+ * Bug fix: Only get index, view, and foreign key information from current
331
+ schema (thanks to [@bhavinkamani](https://github.com/bhavinkamani))
332
+
333
+
334
+ ### Earlier releases
335
+ * 0.4.1 - Bug fix: don't attempt foreign key creation for t.belongs_to ...
336
+ :polymorphic => true
337
+ * 0.4.0 - Add :force for create_view (suggested by [@greglazarev](https://github.com/greglazarev)). cleanups
338
+ by [@betelgeuse](https://github.com/betelgeuse)
339
+ * 0.3.4 - Bug fix: regression causing :default => false to be ignored
340
+ * 0.3.3 - Bug fix: properly handle boolean defaults in mysql
341
+ * 0.3.2 - Bug fix: make sure rake db:schema:load initializes schema_plus
342
+ * 0.3.1 - Bug fix for PostgreSQL schema dump after change_column_default(...
343
+ nil)
344
+ * 0.3.0 - Add :default => expressions (Thanks to Luke Saunders). support
345
+ rails 3.2 and ruby 1.9.3
346
+ * 0.2.1 - Suppress duplicate add_indexes. compatibility with rails
347
+ 3.2.0.rc2
348
+
349
+
350
+ ## History
351
+
352
+ * SchemaPlus is derived from several "Red Hill On Rails" plugins originally
353
+ created by [@harukizaemon](https://github.com/harukizaemon)
354
+
355
+ * SchemaPlus was created in 2011 by [@mlomnicki](https://github.com/mlomnicki) and [@ronen](https://github.com/ronen)
356
+
357
+ * And [lots of
358
+ contributors](https://github.com/lomba/schema_plus/graphs/contributors)
359
+ since then
360
+
@@ -30,6 +30,7 @@ module SchemaPlus::ActiveRecord
30
30
  def get_fk_args(table_name, column_name, column_options = {}, config = {}) #:nodoc:
31
31
 
32
32
  args = nil
33
+ column_name = column_name.to_s
33
34
 
34
35
  if column_options.has_key?(:foreign_key)
35
36
  args = column_options[:foreign_key]
@@ -38,6 +39,7 @@ module SchemaPlus::ActiveRecord
38
39
  return :none if args.has_key?(:references) and not args[:references]
39
40
  end
40
41
 
42
+
41
43
  if column_options.has_key?(:references)
42
44
  references = column_options[:references]
43
45
  return :none unless references
@@ -48,15 +50,14 @@ module SchemaPlus::ActiveRecord
48
50
 
49
51
  return nil if args.nil?
50
52
 
51
- args[:references] ||= case column_name.to_s
52
- when 'parent_id'
53
- [table_name, :id]
54
- when /^(.*)_id$/
55
- references_table_name = ActiveRecord::Base.pluralize_table_names ? $1.to_s.pluralize : $1
56
- [references_table_name, :id]
57
- else
58
- references_table_name = ActiveRecord::Base.pluralize_table_names ? column_name.to_s.pluralize : column_name
53
+ args[:references] ||= table_name if column_name == 'parent_id'
54
+
55
+ args[:references] ||= begin
56
+ table_name = column_name.sub(/_id$/, '')
57
+ table_name = table_name.pluralize if ActiveRecord::Base.pluralize_table_names
58
+ table_name
59
59
  end
60
+
60
61
  args[:references] = [args[:references], :id] unless args[:references].is_a? Array
61
62
 
62
63
  [:on_update, :on_delete, :deferrable].each do |shortcut|
@@ -13,36 +13,25 @@ module SchemaPlus
13
13
  module AbstractAdapter
14
14
  def self.included(base) #:nodoc:
15
15
  base.alias_method_chain :initialize, :schema_plus
16
- base.alias_method_chain :drop_table, :schema_plus
17
16
  end
18
17
 
19
18
  def initialize_with_schema_plus(*args) #:nodoc:
20
19
  initialize_without_schema_plus(*args)
21
- adapter = nil
22
- case adapter_name
23
- # name of MySQL adapter depends on mysql gem
24
- # * with mysql gem adapter is named MySQL
25
- # * with mysql2 gem adapter is named Mysql2
26
- # Here we handle this and hopefully futher adapter names
27
- when /^MySQL/i
28
- adapter = 'MysqlAdapter'
29
- when 'PostgreSQL'
30
- adapter = 'PostgresqlAdapter'
31
- when 'SQLite'
32
- adapter = 'Sqlite3Adapter'
33
- end
34
- if adapter
35
- adapter_module = SchemaPlus::ActiveRecord::ConnectionAdapters.const_get(adapter)
36
- self.class.send(:include, adapter_module) unless self.class.include?(adapter_module)
37
- self.post_initialize if self.respond_to? :post_initialize
38
-
39
- if adapter == 'PostgresqlAdapter'
40
- ::ActiveRecord::ConnectionAdapters::PostgreSQLColumn.send(:include, SchemaPlus::ActiveRecord::ConnectionAdapters::PostgreSQLColumn) unless ::ActiveRecord::ConnectionAdapters::PostgreSQLColumn.include?(SchemaPlus::ActiveRecord::ConnectionAdapters::PostgreSQLColumn)
41
- end
42
- if adapter == 'Sqlite3Adapter'
43
- ::ActiveRecord::ConnectionAdapters::SQLiteColumn.send(:include, SchemaPlus::ActiveRecord::ConnectionAdapters::SQLiteColumn) unless ::ActiveRecord::ConnectionAdapters::SQLiteColumn.include?(SchemaPlus::ActiveRecord::ConnectionAdapters::SQLiteColumn)
44
- end
45
- end
20
+ adapter = case adapter_name
21
+ # name of MySQL adapter depends on mysql gem
22
+ # * with mysql gem adapter is named MySQL
23
+ # * with mysql2 gem adapter is named Mysql2
24
+ # Here we handle this and hopefully futher adapter names
25
+ when /^MySQL/i then 'MysqlAdapter'
26
+ when 'PostgreSQL', 'PostGIS' then 'PostgresqlAdapter'
27
+ when 'SQLite' then 'Sqlite3Adapter'
28
+ end or raise "SchemaPlus: Unsupported adapter name #{adapter_name.inspect}"
29
+ # use 'end or raise' instead of 'else raise' to get
30
+ # 100% C0 code coverage despite having no test case
31
+ # for this.
32
+ adapter_module = SchemaPlus::ActiveRecord::ConnectionAdapters.const_get(adapter)
33
+ self.class.send(:include, adapter_module) unless self.class.include?(adapter_module)
34
+ self.post_initialize if self.respond_to? :post_initialize
46
35
  extend(SchemaPlus::ActiveRecord::ForeignKeys)
47
36
  end
48
37
 
@@ -82,17 +71,16 @@ module SchemaPlus
82
71
  execute "ALTER TABLE #{quote_table_name(table_name)} DROP CONSTRAINT #{foreign_key_name}"
83
72
  end
84
73
 
85
- def drop_table_with_schema_plus(name, options = {}) #:nodoc:
86
- # (NOTE: rails 3.2 accepts only one arg, no options. pre rails
87
- # 3.2, drop_table took an options={} arg that had no effect: but
88
- # create_table(:force=>true) would call drop_table with two args.
89
- # so for backwards compatibility, schema_plus drop_table accepts
90
- # two args. but for forward compatibility with rails 3.2, the
91
- # second arg is not passed along to rails.)
92
- unless ::ActiveRecord::Base.connection.class.include?(SchemaPlus::ActiveRecord::ConnectionAdapters::Sqlite3Adapter)
93
- reverse_foreign_keys(name).each { |foreign_key| remove_foreign_key(foreign_key.table_name, foreign_key.name) }
94
- end
95
- drop_table_without_schema_plus(name)
74
+ # Extends rails' drop_table to include these options:
75
+ # :cascade
76
+ # :if_exists
77
+ #
78
+ def drop_table(name, options = {})
79
+ sql = "DROP TABLE"
80
+ sql += " IF EXISTS" if options[:if_exists]
81
+ sql += " #{quote_table_name(name)}"
82
+ sql += " CASCADE" if options[:cascade]
83
+ execute sql
96
84
  end
97
85
 
98
86
  # called from individual adpaters, after renaming table from old
@@ -149,16 +137,6 @@ module SchemaPlus
149
137
  end
150
138
  end
151
139
 
152
- # This is define in rails 3.x, but not in rails2.x
153
- unless defined? ::ActiveRecord::ConnectionAdapters::SchemaStatements::index_name_exists?
154
- # File activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb, line 403
155
- def index_name_exists?(table_name, index_name, default)
156
- return default unless respond_to?(:indexes)
157
- index_name = index_name.to_s
158
- indexes(table_name).detect { |i| i.name == index_name }
159
- end
160
- end
161
-
162
140
  #####################################################################
163
141
  #
164
142
  # The functions below here are abstract; each subclass should