schema_plus 1.0.1 → 1.1.0

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/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