automodel-sqlserver 0.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.
- 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
|