schema_plus_foreign_keys 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.travis.yml +21 -0
  4. data/Gemfile +5 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +200 -0
  7. data/Rakefile +9 -0
  8. data/gemfiles/Gemfile.base +4 -0
  9. data/gemfiles/activerecord-4.2.0/Gemfile.base +3 -0
  10. data/gemfiles/activerecord-4.2.0/Gemfile.mysql2 +10 -0
  11. data/gemfiles/activerecord-4.2.0/Gemfile.postgresql +10 -0
  12. data/gemfiles/activerecord-4.2.0/Gemfile.sqlite3 +10 -0
  13. data/gemfiles/activerecord-4.2.1/Gemfile.base +3 -0
  14. data/gemfiles/activerecord-4.2.1/Gemfile.mysql2 +10 -0
  15. data/gemfiles/activerecord-4.2.1/Gemfile.postgresql +10 -0
  16. data/gemfiles/activerecord-4.2.1/Gemfile.sqlite3 +10 -0
  17. data/lib/schema_plus/foreign_keys.rb +78 -0
  18. data/lib/schema_plus/foreign_keys/active_record/base.rb +33 -0
  19. data/lib/schema_plus/foreign_keys/active_record/connection_adapters/abstract_adapter.rb +168 -0
  20. data/lib/schema_plus/foreign_keys/active_record/connection_adapters/foreign_key_definition.rb +137 -0
  21. data/lib/schema_plus/foreign_keys/active_record/connection_adapters/mysql2_adapter.rb +126 -0
  22. data/lib/schema_plus/foreign_keys/active_record/connection_adapters/postgresql_adapter.rb +89 -0
  23. data/lib/schema_plus/foreign_keys/active_record/connection_adapters/sqlite3_adapter.rb +77 -0
  24. data/lib/schema_plus/foreign_keys/active_record/connection_adapters/table_definition.rb +108 -0
  25. data/lib/schema_plus/foreign_keys/active_record/migration/command_recorder.rb +29 -0
  26. data/lib/schema_plus/foreign_keys/middleware/dumper.rb +88 -0
  27. data/lib/schema_plus/foreign_keys/middleware/migration.rb +147 -0
  28. data/lib/schema_plus/foreign_keys/middleware/model.rb +15 -0
  29. data/lib/schema_plus/foreign_keys/middleware/mysql.rb +20 -0
  30. data/lib/schema_plus/foreign_keys/middleware/sql.rb +27 -0
  31. data/lib/schema_plus/foreign_keys/version.rb +5 -0
  32. data/lib/schema_plus_foreign_keys.rb +1 -0
  33. data/schema_dev.yml +9 -0
  34. data/schema_plus_foreign_keys.gemspec +31 -0
  35. data/spec/deprecation_spec.rb +161 -0
  36. data/spec/foreign_key_definition_spec.rb +34 -0
  37. data/spec/foreign_key_spec.rb +207 -0
  38. data/spec/migration_spec.rb +570 -0
  39. data/spec/named_schemas_spec.rb +136 -0
  40. data/spec/schema_dumper_spec.rb +257 -0
  41. data/spec/spec_helper.rb +60 -0
  42. data/spec/support/reference.rb +79 -0
  43. metadata +221 -0
@@ -0,0 +1,89 @@
1
+ module SchemaPlus::ForeignKeys
2
+ module ActiveRecord
3
+ module ConnectionAdapters
4
+
5
+ # The Postgresql adapter implements the SchemaPlus::ForeignKeys extensions and
6
+ # enhancements
7
+ module PostgresqlAdapter
8
+
9
+ def rename_table(oldname, newname) #:nodoc:
10
+ super
11
+ rename_foreign_keys(oldname, newname)
12
+ end
13
+
14
+ def foreign_keys(table_name, name = nil) #:nodoc:
15
+ load_foreign_keys(<<-SQL, name)
16
+ SELECT f.conname, pg_get_constraintdef(f.oid), t.relname
17
+ FROM pg_class t, pg_constraint f
18
+ WHERE f.conrelid = t.oid
19
+ AND f.contype = 'f'
20
+ AND t.relname = '#{table_name_without_namespace(table_name)}'
21
+ AND t.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = #{namespace_sql(table_name)} )
22
+ SQL
23
+ end
24
+
25
+ def reverse_foreign_keys(table_name, name = nil) #:nodoc:
26
+ load_foreign_keys(<<-SQL, name)
27
+ SELECT f.conname, pg_get_constraintdef(f.oid), t2.relname
28
+ FROM pg_class t, pg_class t2, pg_constraint f
29
+ WHERE f.confrelid = t.oid
30
+ AND f.conrelid = t2.oid
31
+ AND f.contype = 'f'
32
+ AND t.relname = '#{table_name_without_namespace(table_name)}'
33
+ AND t.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = #{namespace_sql(table_name)} )
34
+ SQL
35
+ end
36
+
37
+ private
38
+
39
+ def unquote(name)
40
+ return name.map { |name| unquote(name) } if name.is_a?(Array)
41
+ name.sub(/^["`](.*)["`]$/, '\1')
42
+ end
43
+
44
+ def namespace_sql(table_name)
45
+ (table_name.to_s =~ /(.*)[.]/) ? "'#{$1}'" : "ANY (current_schemas(false))"
46
+ end
47
+
48
+ def table_name_without_namespace(table_name)
49
+ table_name.to_s.sub /.*[.]/, ''
50
+ end
51
+
52
+ def load_foreign_keys(sql, name = nil) #:nodoc:
53
+ foreign_keys = []
54
+
55
+ query(sql, name).each do |row|
56
+ if row[1] =~ /^FOREIGN KEY \((.+?)\) REFERENCES (.+?)\((.+?)\)( ON UPDATE (.+?))?( ON DELETE (.+?))?( (DEFERRABLE|NOT DEFERRABLE)( (INITIALLY DEFERRED|INITIALLY IMMEDIATE))?)?$/
57
+ name = row[0]
58
+ from_table = unquote(row[2])
59
+ columns = unquote($1.split(', '))
60
+ to_table = unquote($2)
61
+ primary_keys = unquote($3.split(', '))
62
+ on_update = $5
63
+ on_delete = $7
64
+ deferrable = $9 == "DEFERRABLE"
65
+ deferrable = :initially_deferred if ($11 == "INITIALLY DEFERRED" )
66
+ on_update = ForeignKeyDefinition::ACTION_LOOKUP[on_update] || :no_action
67
+ on_delete = ForeignKeyDefinition::ACTION_LOOKUP[on_delete] || :no_action
68
+
69
+ options = { :name => name,
70
+ :on_delete => on_delete,
71
+ :on_update => on_update,
72
+ :column => columns,
73
+ :primary_key => primary_keys,
74
+ :deferrable => deferrable }
75
+
76
+ foreign_keys << ::ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(
77
+ from_table,
78
+ to_table.sub(/^"(.*)"$/, '\1'),
79
+ options)
80
+ end
81
+ end
82
+
83
+ foreign_keys
84
+ end
85
+
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,77 @@
1
+ module SchemaPlus::ForeignKeys
2
+ module ActiveRecord
3
+ module ConnectionAdapters
4
+
5
+ # SchemaPlus::ForeignKeys includes an Sqlite3 implementation of the AbstractAdapter
6
+ # extensions.
7
+ module Sqlite3Adapter
8
+
9
+ # :enddoc:
10
+
11
+ def add_foreign_key(table_name, to_table, options = {})
12
+ raise NotImplementedError, "Sqlite3 does not support altering a table to add foreign key constraints (table #{table_name.inspect} to #{to_table.inspect})"
13
+ end
14
+
15
+ def remove_foreign_key(table_name, *args)
16
+ raise NotImplementedError, "Sqlite3 does not support altering a table to remove foreign key constraints (table #{table_name.inspect} constraint #{args.inspect})"
17
+ end
18
+
19
+ def foreign_keys(table_name, name = nil)
20
+ get_foreign_keys(table_name, name)
21
+ end
22
+
23
+ def reverse_foreign_keys(table_name, name = nil)
24
+ get_foreign_keys(nil, name).select{|definition| definition.to_table == table_name}
25
+ end
26
+
27
+ protected
28
+
29
+ def get_foreign_keys(table_name = nil, name = nil)
30
+ results = execute(<<-SQL, name)
31
+ SELECT name, sql FROM sqlite_master
32
+ WHERE type='table' #{table_name && %" AND name='#{table_name}' "}
33
+ SQL
34
+
35
+ re = %r[
36
+ \b(CONSTRAINT\s+(\S+)\s+)?
37
+ FOREIGN\s+KEY\s* \(\s*[`"](.+?)[`"]\s*\)
38
+ \s*REFERENCES\s*[`"](.+?)[`"]\s*\((.+?)\)
39
+ (\s+ON\s+UPDATE\s+(.+?))?
40
+ (\s*ON\s+DELETE\s+(.+?))?
41
+ (\s*DEFERRABLE(\s+INITIALLY\s+DEFERRED)?)?
42
+ \s*[,)]
43
+ ]x
44
+
45
+ foreign_keys = []
46
+ results.each do |row|
47
+ from_table = row["name"]
48
+ row["sql"].scan(re).each do |d0, name, columns, to_table, primary_keys, d1, on_update, d2, on_delete, deferrable, initially_deferred|
49
+ columns = columns.gsub(/`/, '').split(', ')
50
+
51
+ primary_keys = primary_keys.gsub(/[`"]/, '').split(', ')
52
+ on_update = ForeignKeyDefinition::ACTION_LOOKUP[on_update] || :no_action
53
+ on_delete = ForeignKeyDefinition::ACTION_LOOKUP[on_delete] || :no_action
54
+ deferrable = deferrable ? (initially_deferred ? :initially_deferred : true) : false
55
+
56
+ options = { :name => name,
57
+ :on_update => on_update,
58
+ :on_delete => on_delete,
59
+ :column => columns,
60
+ :primary_key => primary_keys,
61
+ :deferrable => deferrable }
62
+
63
+ foreign_keys << ::ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(
64
+ from_table,
65
+ to_table,
66
+ options)
67
+ end
68
+ end
69
+
70
+ foreign_keys
71
+ end
72
+
73
+ end
74
+
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,108 @@
1
+ module SchemaPlus::ForeignKeys::ActiveRecord::ConnectionAdapters
2
+
3
+ #
4
+ # SchemaPlus::ForeignKeys 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::ForeignKeys 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::ForeignKeys, 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::ForeignKeys 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, :foreign_key => true
48
+ # end
49
+ #
50
+ # <b>NOTE:</b> In the standard configuration, SchemaPlus::ForeignKeys 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
+ # SchemaPlus::ForeignKeys likewise by default automatically creates foreign key constraints for
56
+ # columns defined via <tt>t.references</tt>. However, SchemaPlus::ForeignKeys does not create
57
+ # foreign key constraints if the <tt>:polymorphic</tt> option is true
58
+ #
59
+ # Finally, the configuration for foreign keys can be overriden on a per-table
60
+ # basis by passing Config options to Migration::ClassMethods#create_table, such as
61
+ #
62
+ # create_table :students, :foreign_keys => {:auto_create => false} do
63
+ # t.integer :student_id
64
+ # end
65
+ #
66
+ module TableDefinition
67
+
68
+ attr_accessor :schema_plus_foreign_keys_config #:nodoc:
69
+
70
+ if ActiveRecord.version == Gem::Version.new("4.2.0")
71
+ def foreign_keys
72
+ @foreign_keys ||= []
73
+ end
74
+
75
+ def foreign_keys_for_table(*)
76
+ foreign_keys
77
+ end
78
+ else
79
+ def foreign_keys_for_table(table)
80
+ foreign_keys[table] ||= []
81
+ end
82
+ end
83
+
84
+ def foreign_key(*args) # (column_names, to_table, primary_key=nil, options=nil)
85
+ options = args.extract_options!
86
+ case args.length
87
+ when 1
88
+ to_table = args[0]
89
+ column_names = "#{to_table.to_s.singularize}_id"
90
+ when 2
91
+ column_names, to_table = args
92
+ when 3
93
+ ActiveSupport::Deprecation.warn "positional arg for foreign primary key is deprecated, use :primary_key option instead"
94
+ column_names, to_table, primary_key = args
95
+ options.merge!(:primary_key => primary_key)
96
+ else
97
+ raise ArgumentError, "wrong number of arguments (#{args.length}) for foreign_key(column_names, table_name, options)"
98
+ end
99
+
100
+ options.merge!(:column => column_names)
101
+ options.reverse_merge!(:name => ForeignKeyDefinition.default_name(self.name, column_names))
102
+ fk = ::ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(self.name, AbstractAdapter.proper_table_name(to_table), options)
103
+ foreign_keys_for_table(fk.to_table) << fk
104
+ self
105
+ end
106
+
107
+ end
108
+ end
@@ -0,0 +1,29 @@
1
+ module SchemaPlus::ForeignKeys
2
+ module ActiveRecord
3
+ module Migration
4
+ module CommandRecorder
5
+
6
+ attr_accessor :schema_plus_foreign_keys_config #:nodoc:
7
+
8
+ # seems like this is fixing a rails bug:
9
+ # change_table foo, :bulk => true { |t| t.references :bar }
10
+ # results in an 'unknown method :add_reference_sql' (with mysql2)
11
+ #
12
+ # should track it down separately and submit a patch/fix to rails
13
+ #
14
+ def add_reference(table_name, ref_name, options = {}) #:nodoc:
15
+ polymorphic = options.delete(:polymorphic)
16
+ options[:references] = nil if polymorphic
17
+ # ugh. copying and pasting code from ::ActiveRecord::ConnectionAdapters::SchemaStatements#add_reference
18
+ index_options = options.delete(:index)
19
+ add_column(table_name, "#{ref_name}_id", :integer, options)
20
+ add_column(table_name, "#{ref_name}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) if polymorphic
21
+ add_index(table_name, polymorphic ? %w[id type].map{ |t| "#{ref_name}_#{t}" } : "#{ref_name}_id", index_options.is_a?(Hash) ? index_options : {}) if index_options
22
+
23
+ self
24
+ end
25
+
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,88 @@
1
+ module SchemaPlus::ForeignKeys
2
+ module Middleware
3
+ module Dumper
4
+
5
+ # index and foreign key constraint definitions are dumped
6
+ # inline in the create_table block. (This is done for elegance, but
7
+ # also because Sqlite3 does not allow foreign key constraints to be
8
+ # added to a table after it has been defined.)
9
+
10
+ module Tables
11
+
12
+ def before(env)
13
+
14
+ @inline_fks = Hash.new{ |h, k| h[k] = [] }
15
+ @backref_fks = Hash.new{ |h, k| h[k] = [] }
16
+
17
+ env.connection.tables.each do |table|
18
+ if (fks = env.connection.foreign_keys(table)).any?
19
+ env.dump.data.has_fks = true
20
+ @inline_fks[table] = fks
21
+ env.dump.depends(table, fks.collect(&:to_table))
22
+ end
23
+ end
24
+
25
+ # Normally we dump foreign key constraints inline in the table
26
+ # definitions, both for visual cleanliness and because sqlite3
27
+ # doesn't allow foreign key constraints to be added afterwards.
28
+ # But in case there's a cycle in the constraint references, some
29
+ # constraints will need to be broken out then added later. (Adding
30
+ # constraints later won't work with sqlite3, but that means sqlite3
31
+ # won't let you create cycles in the first place.)
32
+ break_fk_cycles(env) while env.dump.strongly_connected_components.any?{|component| component.size > 1}
33
+
34
+ env.dump.data.inline_fks = @inline_fks
35
+ env.dump.data.backref_fks = @backref_fks
36
+ end
37
+
38
+ # Ignore the foreign key dumps at the end of the schema; we'll put them in/near their tables
39
+ def after(env)
40
+ env.dump.final.reject!(&it =~/foreign_key/)
41
+ end
42
+
43
+ private
44
+
45
+ def break_fk_cycles(env) #:nodoc:
46
+ env.dump.strongly_connected_components.select{|component| component.size > 1}.each do |tables|
47
+ table = tables.sort.last
48
+ backref_fks = @inline_fks[table].select{|fk| tables.include?(fk.to_table)}
49
+ @inline_fks[table] -= backref_fks
50
+ env.dump.dependencies[table] -= backref_fks.collect(&:to_table)
51
+ backref_fks.each do |fk|
52
+ @backref_fks[fk.to_table] << fk
53
+ end
54
+ end
55
+ end
56
+
57
+ module SQLite3
58
+ def after(env)
59
+ env.dump.initial << " PRAGMA FOREIGN_KEYS = ON;\n" if env.dump.data.has_fks
60
+ end
61
+ end
62
+
63
+ end
64
+
65
+ module Table
66
+ def after(env)
67
+ dumped = {}
68
+ env.table.columns.each do |column|
69
+ if (foreign_key = env.dump.data.inline_fks[env.table.name].find(&its.column.to_s == column.name))
70
+ column.add_option foreign_key.to_dump(column: true)
71
+ dumped[foreign_key] = true
72
+ end
73
+ if (foreign_key = env.dump.data.backref_fks.values.flatten.find{|fk| fk.from_table.to_s == env.table.name && fk.column.to_s == column.name})
74
+ column.add_comment "foreign key references #{foreign_key.to_table.inspect} (below)"
75
+ end
76
+ end
77
+ env.table.trailer += env.dump.data.inline_fks[env.table.name].map { |foreign_key|
78
+ foreign_key.to_dump unless dumped[foreign_key] # just in case we missed any. don't think it can happen
79
+ }.compact.sort
80
+ env.table.trailer += env.dump.data.backref_fks[env.table.name].map { |foreign_key|
81
+ foreign_key.to_dump
82
+ }.sort
83
+ end
84
+ end
85
+ end
86
+
87
+ end
88
+ end
@@ -0,0 +1,147 @@
1
+ module SchemaPlus::ForeignKeys
2
+ module Middleware
3
+ module Migration
4
+
5
+ module CreateTable
6
+ def around(env)
7
+ if (original_block = env.block)
8
+ config_options = env.options.delete(:foreign_keys) || {}
9
+ env.block = -> (table_definition) {
10
+ table_definition.schema_plus_foreign_keys_config = SchemaPlus::ForeignKeys.config.merge(config_options)
11
+ original_block.call table_definition
12
+ }
13
+ end
14
+ yield env
15
+ end
16
+ end
17
+
18
+ module Column
19
+
20
+ #
21
+ # Column option shortcuts
22
+ #
23
+ def before(env)
24
+ opts = env.options[:foreign_key]
25
+
26
+ return if opts == false
27
+
28
+ opts = {} if opts == true
29
+
30
+ [:references, :on_update, :on_delete, :deferrable].each do |key|
31
+ (opts||={}).reverse_merge!(key => env.options[key]) if env.options.has_key? key
32
+ end
33
+
34
+ return if opts.nil?
35
+
36
+ if opts.has_key?(:references) && !opts[:references]
37
+ env.options[:foreign_key] = false
38
+ return
39
+ end
40
+
41
+ case opts[:references]
42
+ when nil
43
+ when Array
44
+ table, primary_key = opts[:references]
45
+ opts[:references] = table
46
+ opts[:primary_key] ||= primary_key
47
+ end
48
+
49
+ env.options[:foreign_key] = opts
50
+ end
51
+
52
+ #
53
+ # Add the foreign keys
54
+ #
55
+ def around(env)
56
+ original_options = env.options
57
+ env.options = original_options.dup
58
+
59
+ is_reference = (env.type == :reference)
60
+ is_polymorphic = is_reference && env.options[:polymorphic]
61
+
62
+ # usurp foreign key creation from AR, since it doesn't support
63
+ # all our features
64
+ env.options[:foreign_key] = false
65
+
66
+ yield env
67
+
68
+ return if is_polymorphic or env.implements_reference
69
+
70
+ env.options = original_options
71
+
72
+ add_foreign_keys(env)
73
+
74
+ end
75
+
76
+ private
77
+
78
+ def add_foreign_keys(env)
79
+
80
+ if (reverting = env.caller.is_a?(::ActiveRecord::Migration::CommandRecorder) && env.caller.reverting)
81
+ commands_length = env.caller.commands.length
82
+ end
83
+
84
+ config = (env.caller.try(:schema_plus_foreign_keys_config) || SchemaPlus::ForeignKeys.config)
85
+ fk_opts = get_fk_opts(env, config)
86
+
87
+ # remove existing fk in case of change of fk on existing column
88
+ if env.operation == :change and fk_opts # includes :none for explicitly off
89
+ remove_foreign_key_if_exists(env)
90
+ end
91
+
92
+ fk_opts = nil if fk_opts == :none
93
+
94
+ create_fk(env, fk_opts) if fk_opts
95
+
96
+ if reverting
97
+ rev = []
98
+ while env.caller.commands.length > commands_length
99
+ cmd = env.caller.commands.pop
100
+ rev.unshift cmd unless cmd[0].to_s =~ /^add_/
101
+ end
102
+ env.caller.commands.concat rev
103
+ end
104
+
105
+ end
106
+
107
+ def create_fk(env, fk_opts)
108
+ references = fk_opts.delete(:references)
109
+ case env.caller
110
+ when ::ActiveRecord::ConnectionAdapters::TableDefinition
111
+ env.caller.foreign_key(env.column_name, references, fk_opts)
112
+ else
113
+ env.caller.add_foreign_key(env.table_name, references, fk_opts.merge(:column => env.column_name))
114
+ end
115
+ end
116
+
117
+ def get_fk_opts(env, config)
118
+ opts = env.options[:foreign_key]
119
+ return nil if opts.nil?
120
+ return :none if opts == false
121
+ opts = {} if opts == true
122
+ opts[:references] ||= default_table_name(env)
123
+ opts[:on_update] ||= config.on_update
124
+ opts[:on_delete] ||= config.on_delete
125
+ opts
126
+ end
127
+
128
+ def remove_foreign_key_if_exists(env)
129
+ env.caller.remove_foreign_key(env.table_name.to_s, column: env.column_name.to_s, :if_exists => true)
130
+ end
131
+
132
+ def default_table_name(env)
133
+ if env.column_name.to_s == 'parent_id'
134
+ env.table_name
135
+ else
136
+ name = env.column_name.to_s.sub(/_id$/, '')
137
+ name = name.pluralize if ::ActiveRecord::Base.pluralize_table_names
138
+ name
139
+ end
140
+ end
141
+
142
+ end
143
+
144
+ end
145
+ end
146
+ end
147
+