automodel-sqlserver 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +81 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +5 -0
  7. data/.yardopts +1 -0
  8. data/Gemfile +6 -0
  9. data/Gemfile.lock +82 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +225 -0
  12. data/Rakefile +6 -0
  13. data/automodel-sqlserver.gemspec +39 -0
  14. data/bin/console +14 -0
  15. data/bin/setup +8 -0
  16. data/docs/Automodel.html +161 -0
  17. data/docs/Automodel/AdapterAlreadyRegistered.html +144 -0
  18. data/docs/Automodel/AdapterAlreadyRegisteredError.html +144 -0
  19. data/docs/Automodel/CannotFindOnCompoundPrimaryKey.html +144 -0
  20. data/docs/Automodel/Connectors.html +117 -0
  21. data/docs/Automodel/Error.html +140 -0
  22. data/docs/Automodel/FindOnCompoundPrimaryKeyError.html +144 -0
  23. data/docs/Automodel/Helpers.html +722 -0
  24. data/docs/Automodel/NameCollisionError.html +143 -0
  25. data/docs/Automodel/SchemaInspector.html +1046 -0
  26. data/docs/Automodel/UnregisteredAdapter.html +144 -0
  27. data/docs/_index.html +206 -0
  28. data/docs/class_list.html +51 -0
  29. data/docs/css/common.css +1 -0
  30. data/docs/css/full_list.css +58 -0
  31. data/docs/css/style.css +496 -0
  32. data/docs/file.README.html +333 -0
  33. data/docs/file_list.html +56 -0
  34. data/docs/frames.html +17 -0
  35. data/docs/index.html +333 -0
  36. data/docs/js/app.js +292 -0
  37. data/docs/js/full_list.js +216 -0
  38. data/docs/js/jquery.js +4 -0
  39. data/docs/method_list.html +155 -0
  40. data/docs/top-level-namespace.html +478 -0
  41. data/lib/automodel.rb +132 -0
  42. data/lib/automodel/automodel.rb +4 -0
  43. data/lib/automodel/connectors.rb +8 -0
  44. data/lib/automodel/errors.rb +24 -0
  45. data/lib/automodel/helpers.rb +141 -0
  46. data/lib/automodel/schema_inspector.rb +218 -0
  47. data/lib/automodel/version.rb +10 -0
  48. data/samples/database.yml +38 -0
  49. metadata +259 -0
@@ -0,0 +1,132 @@
1
+ require 'active_record'
2
+ require 'active_support/all'
3
+ require 'securerandom'
4
+
5
+ require 'automodel/automodel'
6
+ require 'automodel/connectors'
7
+ require 'automodel/helpers'
8
+ require 'automodel/version'
9
+
10
+ ## The main (really *only*) entrypoint for the **automodel-sqlserver** gem. This is the method the
11
+ ## end-user calls to trigger a database scrape and model generation.
12
+ ##
13
+ ##
14
+ ## @param spec [Symbol, String, Hash]
15
+ ## The Symbol/String/Hash to pass through to the ActiveRecord connection resolver, as detailed in
16
+ ## [ActiveRecord::ConnectionHandling#establish_connection](http://bit.ly/2JQdA8c). Whether the
17
+ ## given "spec" value is a Hash or is a Symbol/String to run through the ActiveRecord resolver,
18
+ ## the resulting Hash may include the following options (in addition to the actual connection
19
+ ## parameters).
20
+ ##
21
+ ## @option spec [String] :subschema
22
+ ## The name of an additional namespace with which tables in the target database are prefixed.
23
+ ## Intended for use with SQL Server, where a table's fully-qualified name may have an additional
24
+ ## level of namespacing between the database name and the base table name (e.g.
25
+ ## `database.dbo.table`, in which case the subschema would be `"dbo"`).
26
+ ##
27
+ ## @option spec [String] :namespace
28
+ ## A String representing the desired namespace for the generated model classes (e.g. `"NewDB"` or
29
+ ## `"WeirdDB::Models"`). If not given, the generated models will fall under `Kernel` so they are
30
+ ## always available without namespacing, like standard user-defined model classes.
31
+ ##
32
+ ##
33
+ ## @raise [Automodel::ModelNameCollisionError]
34
+ ##
35
+ ## @return [ActiveRecord::Base]
36
+ ## The returned value is an instance of an ActiveRecord::Base subclass. This is the class that
37
+ ## serves as superclass to all of the generated model classes, so that a list of all models can be
38
+ ## easily compiled by calling `#subclasses` on this value.
39
+ ##
40
+ def automodel(spec)
41
+ ## Build out a connection spec Hash from the given value.
42
+ ##
43
+ resolver = ActiveRecord::ConnectionAdapters::ConnectionSpecification::Resolver
44
+ connection_spec = resolver.new(ActiveRecord::Base.configurations).resolve(spec).symbolize_keys
45
+
46
+ ## We need a base class for all of the models we're about to create, but don't want to pollute
47
+ ## ActiveRecord::Base's own connection pool, so we'll need a subclass. This will serve as both
48
+ ## our base class for new models and as the connection pool handler. We're defining it with names
49
+ ## that reflect both uses just to keep the code more legible.
50
+ ##
51
+ connection_handler_name = "CH_#{SecureRandom.uuid.delete('-')}"
52
+ base_class_for_new_models = connection_handler = Class.new(ActiveRecord::Base)
53
+ Automodel::Helpers.register_class(connection_handler, as: connection_handler_name,
54
+ within: :'Automodel::Connectors')
55
+
56
+ ## Establish a connection with the given params.
57
+ ##
58
+ connection_handler.establish_connection(connection_spec)
59
+
60
+ ## Map out the table structures.
61
+ ##
62
+ tables = Automodel::Helpers.map_tables(connection_handler, subschema: connection_spec[:subschema])
63
+
64
+ ## Safeguard against class name collisions.
65
+ ##
66
+ defined_names = Array((connection_spec[:namespace] || :Kernel).to_s.safe_constantize&.constants)
67
+ potential_names = tables.map { |table| table[:model_name].to_sym }
68
+ name_collisions = defined_names & potential_names
69
+ if name_collisions.present?
70
+ connection_handler.connection_pool.disconnect!
71
+ Automodel::Connectors.send(:remove_const, connection_handler_name)
72
+ raise Automodel::NameCollisionError, name_collisions
73
+ end
74
+
75
+ ## Define the table models.
76
+ ##
77
+ tables.each do |table|
78
+ table[:model] = Class.new(base_class_for_new_models) do
79
+ ## We can't assume table properties confom to any standard.
80
+ ##
81
+ self.table_name = table[:name]
82
+ self.primary_key = table[:primary_key]
83
+
84
+ ## Don't allow `#find` for tables with a composite primary key.
85
+ ##
86
+ def find(*args)
87
+ raise Automodel::FindOnCompoundPrimaryKeyError if table[:composite_primary_key]
88
+ super
89
+ end
90
+
91
+ ## Create railsy column name aliases whenever possible.
92
+ ##
93
+ table[:columns].each do |column|
94
+ railsy_name = Automodel::Helpers.railsy_column_name(column)
95
+ unless table[:column_aliases].key? railsy_name
96
+ table[:column_aliases][railsy_name] = column
97
+ alias_attribute(railsy_name, column.name)
98
+ end
99
+ end
100
+ end
101
+
102
+ ## Register the model class.
103
+ ##
104
+ Automodel::Helpers.register_class(table[:model], as: table[:model_name],
105
+ within: connection_spec[:namespace])
106
+ end
107
+
108
+ ## With all models registered, we can safely declare relationships.
109
+ ##
110
+ tables.map { |table| table[:foreign_keys] }.flatten.each do |fk|
111
+ from_table = tables.find { |table| table[:base_name] == fk.from_table.delete('"') }
112
+ next unless from_table.present?
113
+
114
+ to_table = tables.find { |table| table[:base_name] == fk.to_table.delete('"') }
115
+ next unless to_table.present?
116
+
117
+ association_setup = <<~END_OF_HEREDOC
118
+ belongs_to #{to_table[:base_name].to_sym.inspect},
119
+ class_name: #{to_table[:model].to_s.inspect},
120
+ primary_key: #{fk.options[:primary_key].to_sym.inspect},
121
+ foreign_key: #{fk.options[:column].to_sym.inspect}
122
+
123
+ alias #{to_table[:model_name].underscore.to_sym.inspect} #{to_table[:base_name].to_sym.inspect}
124
+ END_OF_HEREDOC
125
+ from_table[:model].class_eval(association_setup, __FILE__, __LINE__)
126
+ end
127
+
128
+ ## There's no obvious value we can return that would be of any use, except maybe the base class,
129
+ ## in case the end user wants to procure a list of all the models (via `#subclasses`).
130
+ ##
131
+ base_class_for_new_models
132
+ end
@@ -0,0 +1,4 @@
1
+ ## The root module for the **automodel-sqlserver** gem.
2
+ ##
3
+ module Automodel
4
+ end
@@ -0,0 +1,8 @@
1
+ require 'automodel/automodel'
2
+
3
+ module Automodel
4
+ ## The module within which all {::automodel}-generated connection handlers are registered.
5
+ ##
6
+ module Connectors
7
+ end
8
+ end
@@ -0,0 +1,24 @@
1
+ require 'automodel/automodel'
2
+
3
+ module Automodel
4
+ ## The base Error class for all **automodel-sqlserver** gem issues.
5
+ ##
6
+ class Error < ::StandardError
7
+ end
8
+
9
+ ## An error resulting from an attempt to register the same adapter name with
10
+ ## {Automodel::SchemaInspector.register_adapter} multiple times.
11
+ ##
12
+ class AdapterAlreadyRegisteredError < Error
13
+ end
14
+
15
+ ## An error resulting from an aborted Automodel due to a class name collision.
16
+ ##
17
+ class NameCollisionError < Error
18
+ end
19
+
20
+ ## An error resulting from calling `#find` on a table with a compound primary key.
21
+ ##
22
+ class FindOnCompoundPrimaryKeyError < Error
23
+ end
24
+ end
@@ -0,0 +1,141 @@
1
+ require 'automodel/automodel'
2
+ require 'automodel/schema_inspector'
3
+
4
+ module Automodel
5
+ ## Houses some helper methods used directly by {::automodel}.
6
+ ##
7
+ module Helpers
8
+ class << self
9
+ ## Takes a connection handler (an object that implements ActiveRecord::ConnectionHandling),
10
+ ## scrapes the target database, and returns a list of the tables' metadata.
11
+ ##
12
+ ##
13
+ ## @param connection_handler [ActiveRecord::ConnectionHandling]
14
+ ## The connection pool/handler to inspect and map out.
15
+ ##
16
+ ## @param subschema [String]
17
+ ## The name of an additional namespace with which tables in the target database are
18
+ ## prefixed, as eplained in {::automodel}.
19
+ ##
20
+ ##
21
+ ## @return [Array<Hash>]
22
+ ## An Array where each value is a Hash representing a table in the target database. Each
23
+ ## such Hash will define the following keys:
24
+ ##
25
+ ## - `:name` (String) -- The table name, prefixed with the subschema name (if one is given).
26
+ ## - `:columns` (Array<ActiveRecord::ConnectionAdapters::Column>)
27
+ ## - `:primary_key` (String, Array<String>)
28
+ ## - `:foreign_keys` (Array<ActiveRecord::ConnectionAdapters::ForeignKeyDefinition>)
29
+ ## - `:base_name` (String) -- The table name, with no subschema.
30
+ ## - `:model_name` (String) -- A Railsy class name for the corresponding model.
31
+ ## - `:composite_primary_key` (true, false)
32
+ ## - `:column_aliases` (Hash<String, ActiveRecord::ConnectionAdapters::Column>)
33
+ ##
34
+ def map_tables(connection_handler, subschema: '')
35
+ ## Normalize the "subschema" name.
36
+ ##
37
+ subschema = "#{subschema}.".sub(%r{\.+$}, '.').sub(%r{^\.}, '')
38
+
39
+ ## Prep the Automodel::SchemaInspector we'll be using.
40
+ ##
41
+ schema_inspector = Automodel::SchemaInspector.new(connection_handler)
42
+
43
+ ## Get as much metadata as possible out of the Automodel::SchemaInspector.
44
+ ##
45
+ schema_inspector.tables.map do |table_name|
46
+ table = {}
47
+
48
+ table[:name] = "#{subschema}#{table_name}"
49
+ table[:columns] = schema_inspector.columns(table[:name])
50
+ table[:primary_key] = schema_inspector.primary_key(table[:name])
51
+ table[:foreign_keys] = schema_inspector.foreign_keys(table[:name])
52
+
53
+ table[:base_name] = table[:name].split('.').last
54
+ table[:model_name] = table[:base_name].underscore.classify
55
+ table[:composite_primary_key] = table[:primary_key].is_a? Array
56
+ table[:column_aliases] = table[:columns].map { |column| [column.name, column] }.to_h
57
+
58
+ table
59
+ end
60
+ end
61
+
62
+ ## Returns a Railsy name for the given column.
63
+ ##
64
+ ##
65
+ ## @param column [ActiveRecord::ConnectionAdapters::Column]
66
+ ## The column for which we want to generate a Railsy name.
67
+ ##
68
+ ##
69
+ ## @return [String]
70
+ ## The given column's name, in Railsy form.
71
+ ##
72
+ ## Note Date/Datetime columns are not suffixed with "_on" or "_at" per Rails norm, as this
73
+ ## can work against you sometimes ("BirthDate" turns into "birth_on"). A future release will
74
+ ## address this by building out a comprehensive list of such names and their correct Railsy
75
+ ## representation, but that is not currently the case.
76
+ ##
77
+ def railsy_column_name(column)
78
+ name = railsy_name(column.name)
79
+ name = name.sub(%r{^is_}, '') if column.type == :boolean
80
+
81
+ name
82
+ end
83
+
84
+ ## Returns the given name in Railsy form.
85
+ ##
86
+ ##
87
+ ## @param name [String, Symbol]
88
+ ## The column name for which we want to generate a Railsy name.
89
+ ##
90
+ ##
91
+ ## @return [String]
92
+ ## The given name, in Railsy form.
93
+ ##
94
+ def railsy_name(name)
95
+ name.to_s.gsub(%r{[^a-z0-9]+}i, '_').underscore
96
+ end
97
+
98
+ ## Registers the given class **as** the given name and **within** the given namespace (if any).
99
+ ##
100
+ ##
101
+ ## @param class_object [Class]
102
+ ## The class to register.
103
+ ##
104
+ ## @param as [String]
105
+ ## The name with which to register the class. Note this should be a base name (no "::").
106
+ ##
107
+ ## @param within [String, Symbol, Module, Class]
108
+ ## The module/class under which the given class should be registered. If the named module or
109
+ ## class does not exist, as many nested modules as needed are declared so the class can be
110
+ ## registered as requested.
111
+ ##
112
+ ## e.g. Calling this method with an "as" value of `"Sample"` and a "within" value of
113
+ ## `"Many::Levels::Deep"` will: check for module/class "Many" and create one as a
114
+ ## Module if it doesn't already exist; then check for a module/class "Many::Levels" and
115
+ ## create a "Levels" Module within `Many` if it doesn't already exist; then check for a
116
+ ## module/class "Many::Levels::Deep" and create a "Deep" Module within `Many::Levels`
117
+ ## if it doesn't already exist; and finally register the given class as "Sample" within
118
+ ## `Many::Levels::Deep`.
119
+ ##
120
+ ##
121
+ ## @return [Class]
122
+ ## The newly-registered class (the same value as the originally-submitted "class_object").
123
+ ##
124
+ def register_class(class_object, as:, within: nil)
125
+ components = within.to_s.split('::').compact.map(&:to_sym)
126
+ components.unshift(:Kernel) unless components.first.to_s.safe_constantize.present?
127
+
128
+ namespace = components.shift.to_s.constantize
129
+ components.each do |component|
130
+ namespace = if component.in? namespace.constants
131
+ namespace.const_get(component)
132
+ else
133
+ namespace.const_set(component, Module.new)
134
+ end
135
+ end
136
+
137
+ namespace.const_set(as, class_object)
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,218 @@
1
+ require 'active_support/all'
2
+ require 'securerandom'
3
+
4
+ require 'automodel/automodel'
5
+ require 'automodel/errors'
6
+
7
+ module Automodel
8
+ ## A utility object that issues the actual database inspection commands and returns the table,
9
+ ## column, primary-key, and foreign-key data.
10
+ ##
11
+ class SchemaInspector
12
+ ## rubocop:disable all
13
+
14
+ ## Class-Instance variable: `known_adapters` is a Hash of adapters registered via
15
+ ## {Automodel::SchemaInspector.register_adapter}.
16
+ ##
17
+ @known_adapters = {}
18
+ def self.known_adapters; @known_adapters; end
19
+ def known_adapters; self.class.known_adapters; end
20
+ ## rubocop:enable all
21
+
22
+ ## "Registers" an adapter with the Automodel::SchemaInspector. This allows for alternate
23
+ ## mechanisms of procuring lists of tables, columns, primary keys, and/or foreign keys from an
24
+ ## adapter that may not itself support `#tables`/`#columns`/`#primary_key`/`#foreign_keys`.
25
+ ##
26
+ ##
27
+ ## @param adapter [String, Symbol]
28
+ ## The "adapter" value used to match that given in the connection spec. It is with this value
29
+ ## that the adapter being registered is matched to an existing database pool/connection.
30
+ ##
31
+ ## @param tables [Proc]
32
+ ## The Proc to `#call` to request a list of table names. The Proc will be called with one
33
+ ## parameter: a database connection.
34
+ ##
35
+ ## @param columns [Proc]
36
+ ## The Proc to `#call` to request a list of columns for a specific table. The Proc will be
37
+ ## called with two parameters: a database connection and a table name.
38
+ ##
39
+ ## @param primary_key [Proc]
40
+ ## The Proc to `#call` to request the primary key for a specific table. The Proc will be
41
+ ## called with two parameters: a database connection and a table name.
42
+ ##
43
+ ## @param foreign_keys [Proc]
44
+ ## The Proc to `#call` to request a list of foreign keys for a specific table. The Proc will
45
+ ## be called with two parameters: a database connection and a table name.
46
+ ##
47
+ ##
48
+ ## @raise [Automodel::AdapterAlreadyRegisteredError]
49
+ ##
50
+ def self.register_adapter(adapter:, tables:, columns:, primary_key:, foreign_keys: nil)
51
+ adapter = adapter.to_sym.downcase
52
+ raise Automodel::AdapterAlreadyRegisteredError, adapter if known_adapters.key? adapter
53
+
54
+ known_adapters[adapter] = { tables: tables,
55
+ columns: columns,
56
+ primary_key: primary_key,
57
+ foreign_keys: foreign_keys }
58
+ end
59
+
60
+ ## @param connection_handler [ActiveRecord::ConnectionHandling]
61
+ ## The connection pool/handler (an object that implements ActiveRecord::ConnectionHandling) to
62
+ ## inspect and map out.
63
+ ##
64
+ def initialize(connection_handler)
65
+ @connection = connection_handler.connection
66
+ adapter = connection_handler.connection_pool.spec.config[:adapter]
67
+
68
+ @registration = known_adapters[adapter.to_sym] || {}
69
+ end
70
+
71
+ ## Returns a list of table names in the target database.
72
+ ##
73
+ ## If a matching Automodel::SchemaInspector registration is found for the connection's adapter,
74
+ ## and that registration specified a `:tables` Proc, the Proc is called. Otherwise, the standard
75
+ ## connection `#tables` is returned.
76
+ ##
77
+ ##
78
+ ## @return [Array<String>]
79
+ ##
80
+ def tables
81
+ @tables ||= if @registration[:tables].present?
82
+ @registration[:tables].call(@connection)
83
+ else
84
+ @connection.tables
85
+ end
86
+ end
87
+
88
+ ## Returns a list of columns for the given table.
89
+ ##
90
+ ## If a matching Automodel::SchemaInspector registration is found for the connection's adapter,
91
+ ## and that registration specified a `:columns` Proc, the Proc is called. Otherwise, the
92
+ ## standard connection `#columns` is returned.
93
+ ##
94
+ ##
95
+ ## @param table_name [String]
96
+ ## The table whose columns should be fetched.
97
+ ##
98
+ ##
99
+ ## @return [Array<ActiveRecord::ConnectionAdapters::Column>]
100
+ ##
101
+ def columns(table_name)
102
+ table_name = table_name.to_s
103
+
104
+ @columns ||= {}
105
+ @columns[table_name] ||= if @registration[:columns].present?
106
+ @registration[:columns].call(@connection, table_name)
107
+ else
108
+ @connection.columns(table_name)
109
+ end
110
+ end
111
+
112
+ ## Returns the primary key for the given table.
113
+ ##
114
+ ## If a matching Automodel::SchemaInspector registration is found for the connection's adapter,
115
+ ## and that registration specified a `:primary_key` Proc, the Proc is called. Otherwise, the
116
+ ## standard connection `#primary_key` is returned.
117
+ ##
118
+ ##
119
+ ## @param table_name [String]
120
+ ## The table whose primary key should be fetched.
121
+ ##
122
+ ##
123
+ ## @return [String, Array<String>]
124
+ ##
125
+ def primary_key(table_name)
126
+ table_name = table_name.to_s
127
+
128
+ @primary_keys ||= {}
129
+ @primary_keys[table_name] ||= if @registration[:primary_key].present?
130
+ @registration[:primary_key].call(@connection, table_name)
131
+ else
132
+ @connection.primary_key(table_name)
133
+ end
134
+ end
135
+
136
+ ## Returns a list of foreign keys for the given table.
137
+ ##
138
+ ## If a matching Automodel::SchemaInspector registration is found for the connection's adapter,
139
+ ## and that registration specified a `:foreign_keys` Proc, the Proc is called. Otherwise, the
140
+ ## standard connection `#foreign_keys` is attempted. If that call to ``#foreign_keys` raises a
141
+ ## ::NoMethodError or ::NotImplementedError, a best-effort attempt is made to build a list of
142
+ ## foreign keys based on table and column names.
143
+ ##
144
+ ##
145
+ ## @param table_name [String]
146
+ ## The table whose foreign keys should be fetched.
147
+ ##
148
+ ##
149
+ ## @return [Array<ActiveRecord::ConnectionAdapters::ForeignKeyDefinition>]
150
+ ##
151
+ def foreign_keys(table_name)
152
+ table_name = table_name.to_s
153
+
154
+ @foreign_keys ||= {}
155
+ @foreign_keys[table_name] ||= begin
156
+ if @registration[:foreign_keys].present?
157
+ @registration[:foreign_keys].call(@connection, table_name)
158
+ else
159
+ begin
160
+ @connection.foreign_keys(table_name)
161
+ rescue ::NoMethodError, ::NotImplementedError
162
+ ## Not all ActiveRecord adapters support `#foreign_keys`. When this happens, we'll make
163
+ ## a best-effort attempt to intuit relationships from the table and column names.
164
+ ##
165
+ columns(table_name).map do |column|
166
+ id_pattern = %r{(?:_id|Id)$}
167
+ next unless column.name =~ id_pattern
168
+
169
+ target_table = column.name.sub(id_pattern, '')
170
+ next unless target_table.in? tables
171
+
172
+ target_column = primary_key(qualified_name(target_table, context: table_name))
173
+ next unless target_column.in? ['id', 'Id', 'ID', column.name]
174
+
175
+ ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(
176
+ table_name.split('.').last,
177
+ target_table,
178
+ name: "FK_#{SecureRandom.uuid.delete('-')}",
179
+ column: column.name,
180
+ primary_key: target_column,
181
+ on_update: nil,
182
+ on_delete: nil
183
+ )
184
+ end.compact
185
+ end
186
+ end
187
+ end
188
+ end
189
+
190
+ private
191
+
192
+ ## Returns a qualified table name.
193
+ ##
194
+ ##
195
+ ## @param table_name [String]
196
+ ## The name to qualify.
197
+ ##
198
+ ## @param context [String]
199
+ ## The name of an existing table from whose namespace we want to be able to reach the first
200
+ ## table.
201
+ ##
202
+ ##
203
+ ## @return [String]
204
+ ##
205
+ def qualified(table_name, context:)
206
+ return table_name if table_name['.'].present?
207
+ return table_name if context['.'].blank?
208
+
209
+ "#{context.sub(%r{[^.]*$}, '')}#{table_name}"
210
+ end
211
+
212
+ ## Returns an unqualified table name.
213
+ ##
214
+ def unqualified(table_name)
215
+ table_name.split('.').last
216
+ end
217
+ end
218
+ end