automodel-sqlserver 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +81 -0
- data/.ruby-version +1 -0
- data/.travis.yml +5 -0
- data/.yardopts +1 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +82 -0
- data/LICENSE.txt +21 -0
- data/README.md +225 -0
- data/Rakefile +6 -0
- data/automodel-sqlserver.gemspec +39 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/docs/Automodel.html +161 -0
- data/docs/Automodel/AdapterAlreadyRegistered.html +144 -0
- data/docs/Automodel/AdapterAlreadyRegisteredError.html +144 -0
- data/docs/Automodel/CannotFindOnCompoundPrimaryKey.html +144 -0
- data/docs/Automodel/Connectors.html +117 -0
- data/docs/Automodel/Error.html +140 -0
- data/docs/Automodel/FindOnCompoundPrimaryKeyError.html +144 -0
- data/docs/Automodel/Helpers.html +722 -0
- data/docs/Automodel/NameCollisionError.html +143 -0
- data/docs/Automodel/SchemaInspector.html +1046 -0
- data/docs/Automodel/UnregisteredAdapter.html +144 -0
- data/docs/_index.html +206 -0
- data/docs/class_list.html +51 -0
- data/docs/css/common.css +1 -0
- data/docs/css/full_list.css +58 -0
- data/docs/css/style.css +496 -0
- data/docs/file.README.html +333 -0
- data/docs/file_list.html +56 -0
- data/docs/frames.html +17 -0
- data/docs/index.html +333 -0
- data/docs/js/app.js +292 -0
- data/docs/js/full_list.js +216 -0
- data/docs/js/jquery.js +4 -0
- data/docs/method_list.html +155 -0
- data/docs/top-level-namespace.html +478 -0
- data/lib/automodel.rb +132 -0
- data/lib/automodel/automodel.rb +4 -0
- data/lib/automodel/connectors.rb +8 -0
- data/lib/automodel/errors.rb +24 -0
- data/lib/automodel/helpers.rb +141 -0
- data/lib/automodel/schema_inspector.rb +218 -0
- data/lib/automodel/version.rb +10 -0
- data/samples/database.yml +38 -0
- metadata +259 -0
data/lib/automodel.rb
ADDED
@@ -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,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
|