active_record-mti 0.3.0.pre.rc1 → 0.3.0.pre.rc2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +9 -8
- data/lib/active_record/mti/calculations.rb +3 -4
- data/lib/active_record/mti/connection_adapters/postgresql/schema_statements.rb +2 -2
- data/lib/active_record/mti/inheritance.rb +57 -114
- data/lib/active_record/mti/model_schema.rb +22 -33
- data/lib/active_record/mti/query_methods.rb +4 -17
- data/lib/active_record/mti/registry.rb +23 -0
- data/lib/active_record/mti/schema_dumper.rb +45 -148
- data/lib/active_record/mti/version.rb +1 -1
- data/lib/active_record/mti.rb +29 -4
- data/lib/core_ext/hash.rb +7 -0
- data/spec/active_record/mti/{inheritence_spec.rb → inheritance_spec.rb} +31 -14
- data/spec/active_record/mti/model_schema_spec.rb +11 -0
- data/spec/active_record/mti/schema_dumper_spec.rb +22 -0
- data/spec/active_record/sti/inheritance_spec.rb +24 -0
- data/spec/schema.rb +2 -2
- data/spec/spec_helper.rb +1 -1
- data/spec/support/models.rb +6 -2
- metadata +13 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d60ec74fdc811ec4b99d12daed96d8d44989026b
|
4
|
+
data.tar.gz: 5e11732fcfec73c329050915107a088876886b37
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 62ce99ba9014e671d53c84b1bab0ac37de5dc7ee3317767b27b3dcc8084b6f544b0a1ffe25674a83723b9f8ee8e19d4ccc0fa22ee728df9b21d2186075ee8ecf
|
7
|
+
data.tar.gz: 8fac323f73f8d83aeb417a28243c711ff3073d58a541179aaa5ccb9f985fe2329917c286368a17b9d50426304eb10d3661352ad6f0b1896ed4106e6e17bfdaa7
|
data/README.md
CHANGED
@@ -27,26 +27,27 @@ Or install it yourself as:
|
|
27
27
|
|
28
28
|
### Application Code
|
29
29
|
|
30
|
-
ActiveRecord
|
31
|
-
|
32
|
-
* You need to specify which model represents the base of your multi table inheritance tree. To do so, add `uses_mti` to the model definition of the base class.
|
33
|
-
* The default query of "*" is changed to include the OID of each row for subclass discrimination. The default select will be `SELECT "accounts"."tableoid" AS tableoid, "accounts".*` (for example)
|
30
|
+
In most cases, you shouldn't have to do anything beyond installing the gem. `ActiveRecord::MTI` will do it's best to determine the nature of inheritance in your models. If your models map to their own tables, `ActiveRecord::MTI` will step in and make sure inheritance is treated appropriately. Otherwise it will gracefully aquiece to `ActiveRecord`'s built-in `STI`.
|
34
31
|
|
35
32
|
```ruby
|
36
33
|
class Account < ::ActiveRecord::Base
|
37
|
-
|
38
|
-
|
34
|
+
# ...
|
39
35
|
end
|
40
36
|
|
41
37
|
class User < Account
|
42
|
-
|
38
|
+
# ...
|
43
39
|
end
|
44
40
|
|
45
41
|
class Developer < Account
|
46
|
-
|
42
|
+
# ...
|
47
43
|
end
|
48
44
|
```
|
49
45
|
|
46
|
+
`ActiveRecord` queries work as usual with the following differences:
|
47
|
+
|
48
|
+
- The default query of "\*" is changed to include the OID of each row for subclass discrimination. The default select will be `SELECT "accounts"."tableoid" AS tableoid, "accounts".*` (for example)
|
49
|
+
|
50
|
+
Note
|
50
51
|
### Migrations
|
51
52
|
|
52
53
|
In your migrations define a table to inherit from another table:
|
@@ -1,20 +1,19 @@
|
|
1
1
|
module ActiveRecord
|
2
2
|
module MTI
|
3
3
|
module Calculations
|
4
|
-
extend ActiveSupport::Concern
|
5
4
|
|
6
5
|
private
|
7
6
|
|
8
7
|
def perform_calculation(*args)
|
9
|
-
|
8
|
+
swap_and_restore_tableoid_cast(true) do
|
10
9
|
super
|
11
10
|
end
|
12
11
|
end
|
13
12
|
|
14
|
-
def swap_and_restore_tableoid_cast(value
|
13
|
+
def swap_and_restore_tableoid_cast(value)
|
15
14
|
orignal_value = Thread.current['skip_tableoid_cast']
|
16
15
|
Thread.current['skip_tableoid_cast'] = value
|
17
|
-
return_value = yield
|
16
|
+
return_value = yield if block_given?
|
18
17
|
Thread.current['skip_tableoid_cast'] = orignal_value
|
19
18
|
return return_value
|
20
19
|
end
|
@@ -21,13 +21,13 @@ module ActiveRecord
|
|
21
21
|
options.delete(:primary_key)
|
22
22
|
end
|
23
23
|
|
24
|
-
if schema = options.delete(:schema)
|
24
|
+
if (schema = options.delete(:schema))
|
25
25
|
# If we specify a schema then we only create it if it doesn't exist
|
26
26
|
# and we only force create it if only the specific schema is in the search path
|
27
27
|
table_name = %Q("#{schema}"."#{table_name}")
|
28
28
|
end
|
29
29
|
|
30
|
-
if inherited_table = options.delete(:inherits)
|
30
|
+
if (inherited_table = options.delete(:inherits))
|
31
31
|
# options[:options] = options[:options].sub("INHERITS", "() INHERITS") if td.columns.empty?
|
32
32
|
options[:options] = [%Q(INHERITS ("#{inherited_table}")), options[:options]].compact.join
|
33
33
|
end
|
@@ -1,183 +1,126 @@
|
|
1
|
-
require 'active_support/concern'
|
2
|
-
|
3
1
|
module ActiveRecord
|
4
2
|
# == Multi-Table Inheritance
|
5
3
|
module MTI
|
6
4
|
module Inheritance
|
7
|
-
extend ActiveSupport::Concern
|
8
|
-
|
9
|
-
included do
|
10
|
-
@@mti_tableoids = {}
|
11
|
-
scope :discern_inheritance, -> {
|
12
5
|
|
13
|
-
|
6
|
+
def self.prepended(subclass)
|
7
|
+
subclass.extend(ClassMethods)
|
8
|
+
class << subclass
|
9
|
+
class_attribute :mti_type_column
|
10
|
+
class_attribute :tableoid_column
|
11
|
+
end
|
14
12
|
end
|
15
13
|
|
16
14
|
module ClassMethods
|
15
|
+
def has_tableoid_column?
|
16
|
+
tableoid_column != false
|
17
|
+
end
|
17
18
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
def uses_mti(custom_table_name = nil, inheritance_column = nil)
|
23
|
-
self.inheritance_column = inheritance_column
|
24
|
-
|
25
|
-
@uses_mti = true
|
26
|
-
@tableoid_column = nil
|
19
|
+
def inherited(subclass)
|
20
|
+
super
|
21
|
+
subclass.using_multi_table_inheritance?
|
27
22
|
end
|
28
23
|
|
29
|
-
def
|
30
|
-
|
24
|
+
def uses_mti(*args)
|
25
|
+
# ActiveRecord::MTI.logger.
|
26
|
+
warn "DEPRECATED - `uses_mti` is no longer needed (nor has any effect)"
|
31
27
|
end
|
32
28
|
|
33
|
-
def
|
34
|
-
|
29
|
+
def using_multi_table_inheritance?
|
30
|
+
mti = ActiveRecord::MTI::Registry.tableoid?(self)
|
31
|
+
return (mti != false) unless mti == nil
|
35
32
|
|
36
|
-
if
|
37
|
-
|
38
|
-
|
33
|
+
if (mti = check_inheritance_of(@table_name))
|
34
|
+
if (self != base_class && self.table_name == base_class.table_name)
|
35
|
+
mti = false
|
36
|
+
else
|
37
|
+
mti = detect_tableoid(table_name)
|
39
38
|
end
|
40
39
|
end
|
41
40
|
|
42
|
-
|
43
|
-
end
|
41
|
+
ActiveRecord::MTI::Registry[self] = mti
|
44
42
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
def mti_type_column
|
50
|
-
@mti_type_column
|
51
|
-
end
|
43
|
+
descendants.each do |d|
|
44
|
+
d.using_multi_table_inheritance?
|
45
|
+
end
|
52
46
|
|
53
|
-
|
54
|
-
@mti_type_column = value
|
47
|
+
return mti && mti != false
|
55
48
|
end
|
56
49
|
|
57
50
|
private
|
58
51
|
|
59
|
-
def check_inheritance_of(table_name)
|
60
|
-
ActiveRecord::MTI.logger.debug
|
61
|
-
|
62
|
-
|
63
|
-
ActiveRecord::MTI.logger.debug "Checking inheritance for #{table_name}"
|
52
|
+
def check_inheritance_of(table_name, table_schema = 'public')
|
53
|
+
ActiveRecord::MTI.logger.debug("Trying to check inheritance of table with no table name (#{self})") and return nil unless table_name
|
54
|
+
ActiveRecord::MTI.logger.debug "Checking inheritance for #{table_schema}.#{table_name}"
|
64
55
|
|
65
56
|
result = connection.execute <<-SQL
|
66
57
|
SELECT EXISTS (
|
67
58
|
SELECT 1
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
59
|
+
FROM pg_catalog.pg_inherits AS i
|
60
|
+
JOIN information_schema.tables AS t ON t.table_schema = '#{table_schema}' AND t.table_name = '#{table_name}'
|
61
|
+
LEFT JOIN pg_catalog.pg_rewrite AS r ON r.ev_class = t.table_name::regclass::oid
|
62
|
+
LEFT JOIN pg_catalog.pg_depend AS d ON d.objid = r.oid
|
63
|
+
LEFT JOIN pg_catalog.pg_class AS c ON c.oid = d.refobjid
|
64
|
+
WHERE i.inhrelid = COALESCE(c.relname, t.table_name)::regclass::oid
|
65
|
+
OR i.inhparent = COALESCE(c.relname, t.table_name)::regclass::oid
|
74
66
|
) AS uses_inheritance;
|
75
67
|
SQL
|
76
68
|
|
77
|
-
|
78
|
-
|
79
|
-
register_tableoid(table_name) if uses_inheritance
|
80
|
-
|
81
|
-
@mti_setup = true
|
82
|
-
# Some versions of PSQL return {"?column?"=>"t"}
|
83
|
-
# instead of {"exists"=>"t"}, so we're saying screw it,
|
84
|
-
# just give me the first value of whatever is returned
|
85
|
-
|
86
|
-
# Ensure a boolean is returned
|
87
|
-
return uses_inheritance == true
|
69
|
+
return ActiveRecord::MTI.testify(result.try(:first)['uses_inheritance']) == true
|
88
70
|
end
|
89
71
|
|
90
|
-
def
|
72
|
+
def detect_tableoid(table_name, table_schema = 'public')
|
91
73
|
|
92
74
|
tableoid_query = connection.execute(<<-SQL
|
93
|
-
SELECT
|
94
|
-
SELECT 1
|
75
|
+
SELECT 1 AS has_tableoid_column, t.table_name::regclass::oid as tableoid
|
95
76
|
FROM pg_catalog.pg_attribute
|
96
|
-
|
77
|
+
JOIN information_schema.tables t ON t.table_schema = '#{table_schema}' AND t.table_name = '#{table_name}'
|
78
|
+
WHERE attrelid = t.table_name::regclass
|
97
79
|
AND attname = 'tableoid'
|
98
|
-
AND NOT attisdropped
|
99
|
-
)) AS has_tableoid_column
|
80
|
+
AND NOT attisdropped;
|
100
81
|
SQL
|
101
82
|
).first
|
102
|
-
|
103
|
-
|
83
|
+
|
84
|
+
tableoid = tableoid_query.try(:[], 'tableoid') || false
|
85
|
+
self.tableoid_column = ActiveRecord::MTI.testify(tableoid_query.try(:[], 'has_tableoid_column'))
|
104
86
|
|
105
87
|
if (has_tableoid_column?)
|
106
|
-
ActiveRecord::MTI.logger.debug "#{table_name} has tableoid column! (#{tableoid})"
|
88
|
+
ActiveRecord::MTI.logger.debug "#{table_schema}.#{table_name} has tableoid column! (#{tableoid})"
|
107
89
|
add_tableoid_column
|
108
|
-
|
90
|
+
self.mti_type_column = arel_table[:tableoid]
|
109
91
|
else
|
110
|
-
|
92
|
+
self.mti_type_column = nil
|
111
93
|
end
|
112
94
|
|
113
|
-
|
95
|
+
tableoid
|
114
96
|
end
|
115
97
|
|
116
98
|
# Called by +instantiate+ to decide which class to use for a new
|
117
99
|
# record instance. For single-table inheritance, we check the record
|
118
100
|
# for a +type+ column and return the corresponding class.
|
119
101
|
def discriminate_class_for_record(record)
|
120
|
-
if using_multi_table_inheritance?
|
121
|
-
find_mti_class(record) ||
|
122
|
-
elsif using_single_table_inheritance?(record)
|
123
|
-
find_sti_class(record[inheritance_column])
|
102
|
+
if using_multi_table_inheritance?
|
103
|
+
ActiveRecord::MTI::Registry.find_mti_class(record['tableoid']) || self
|
124
104
|
else
|
125
105
|
super
|
126
106
|
end
|
127
107
|
end
|
128
108
|
|
129
|
-
# Search descendants for one who's table_name is equal to the returned tableoid.
|
130
|
-
# This indicates the class of the record
|
131
|
-
def find_mti_class(record)
|
132
|
-
if (has_tableoid_column?)
|
133
|
-
Inheritance.find_mti(record['tableoid'])
|
134
|
-
else
|
135
|
-
self
|
136
|
-
end
|
137
|
-
end
|
138
|
-
|
139
109
|
# Type condition only applies if it's STI, otherwise it's
|
140
110
|
# done for free by querying the inherited table in MTI
|
141
111
|
def type_condition(table = arel_table)
|
142
|
-
if using_multi_table_inheritance?
|
143
|
-
|
144
|
-
else
|
145
|
-
sti_column = table[inheritance_column]
|
146
|
-
sti_names = ([self] + descendants).map { |model| model.sti_name }
|
147
|
-
|
148
|
-
sti_column.in(sti_names)
|
149
|
-
end
|
112
|
+
return nil if using_multi_table_inheritance?
|
113
|
+
super
|
150
114
|
end
|
151
115
|
|
152
116
|
def add_tableoid_column
|
153
117
|
if self.respond_to? :attribute
|
154
|
-
self.attribute :tableoid,
|
118
|
+
self.attribute :tableoid, ActiveRecord::MTI.oid_class.new
|
155
119
|
else
|
156
|
-
columns.unshift ActiveRecord::ConnectionAdapters::PostgreSQLColumn.new('tableoid', nil, ActiveRecord::
|
120
|
+
columns.unshift ActiveRecord::ConnectionAdapters::PostgreSQLColumn.new('tableoid', nil, ActiveRecord::MTI.oid_class.new, "oid", false)
|
157
121
|
end
|
158
122
|
end
|
159
|
-
|
160
|
-
# Rails decided to make a breaking change in it's 4.x series :P
|
161
|
-
def get_integer_oid_class
|
162
|
-
::ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Integer
|
163
|
-
rescue NameError
|
164
|
-
begin
|
165
|
-
::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::OID::Integer
|
166
|
-
rescue NameError
|
167
|
-
::ActiveModel::Type::Integer
|
168
|
-
end
|
169
|
-
end
|
170
|
-
|
171
123
|
end
|
172
|
-
|
173
|
-
def self.add_mti(tableoid, klass)
|
174
|
-
@@mti_tableoids[tableoid.to_s.to_sym] = klass
|
175
|
-
end
|
176
|
-
|
177
|
-
def self.find_mti(tableoid)
|
178
|
-
@@mti_tableoids[tableoid.to_s.to_sym]
|
179
|
-
end
|
180
|
-
|
181
124
|
end
|
182
125
|
end
|
183
126
|
end
|
@@ -1,35 +1,15 @@
|
|
1
1
|
module ActiveRecord
|
2
2
|
module MTI
|
3
3
|
module ModelSchema
|
4
|
-
extend ActiveSupport::Concern
|
5
4
|
|
5
|
+
def self.prepended(base)
|
6
|
+
base.extend(ClassMethods)
|
7
|
+
end
|
6
8
|
|
7
9
|
module ClassMethods
|
8
|
-
|
9
|
-
# Computes the table name, (re)sets it internally, and returns it.
|
10
|
-
def reset_table_name #:nodoc:
|
11
|
-
self.table_name = if abstract_class?
|
12
|
-
superclass == Base ? nil : superclass.table_name
|
13
|
-
elsif superclass.abstract_class?# || superclass.using_multi_table_inheritance?
|
14
|
-
superclass.table_name || compute_table_name
|
15
|
-
else
|
16
|
-
compute_table_name
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
10
|
# Computes and returns a table name according to default conventions.
|
21
11
|
def compute_table_name
|
22
|
-
|
23
|
-
if self == base
|
24
|
-
# Nested classes are prefixed with singular parent table name.
|
25
|
-
if parent < Base && !parent.abstract_class?
|
26
|
-
contained = parent.table_name
|
27
|
-
contained = contained.singularize if parent.pluralize_table_names
|
28
|
-
contained += '_'
|
29
|
-
end
|
30
|
-
|
31
|
-
"#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{full_table_name_suffix}"
|
32
|
-
elsif uses_mti?
|
12
|
+
if self != base_class
|
33
13
|
# Nested classes are prefixed with singular parent table name.
|
34
14
|
if superclass < Base && !superclass.abstract_class?
|
35
15
|
contained = superclass.table_name
|
@@ -37,30 +17,39 @@ module ActiveRecord
|
|
37
17
|
contained += '/'
|
38
18
|
end
|
39
19
|
|
40
|
-
"#{full_table_name_prefix}#{contained}#{decorated_table_name(name)}#{full_table_name_suffix}"
|
20
|
+
potential_table_name = "#{full_table_name_prefix}#{contained}#{decorated_table_name(name)}#{full_table_name_suffix}"
|
21
|
+
|
22
|
+
if check_inheritance_of(potential_table_name)
|
23
|
+
potential_table_name
|
24
|
+
else
|
25
|
+
superclass.table_name
|
26
|
+
end
|
41
27
|
else
|
42
|
-
|
43
|
-
superclass.table_name
|
28
|
+
super
|
44
29
|
end
|
45
30
|
end
|
46
31
|
|
47
|
-
def full_table_name_prefix #:nodoc:
|
48
|
-
(parents.detect{ |p| p.respond_to?(:table_name_prefix) } || self).table_name_prefix
|
49
|
-
end
|
50
|
-
|
51
32
|
def full_table_name_suffix #:nodoc:
|
52
|
-
|
33
|
+
super
|
34
|
+
rescue NoMethodError
|
35
|
+
full_table_name_rescue(:table_name_suffix)
|
53
36
|
end
|
54
37
|
|
55
38
|
private
|
56
39
|
|
40
|
+
def full_table_name_rescue(which)
|
41
|
+
(parents.detect{ |p| p.respond_to?(which) } || self).send(which)
|
42
|
+
end
|
43
|
+
|
57
44
|
# Guesses the table name, but does not decorate it with prefix and suffix information.
|
58
45
|
def decorated_table_name(class_name = base_class.name)
|
46
|
+
super
|
47
|
+
rescue NoMethodError
|
59
48
|
table_name = class_name.to_s.underscore
|
60
49
|
pluralize_table_names ? table_name.pluralize : table_name
|
61
50
|
end
|
62
|
-
|
63
51
|
end
|
52
|
+
|
64
53
|
end
|
65
54
|
end
|
66
55
|
end
|
@@ -1,7 +1,6 @@
|
|
1
1
|
module ActiveRecord
|
2
2
|
module MTI
|
3
3
|
module QueryMethods
|
4
|
-
extend ActiveSupport::Concern
|
5
4
|
|
6
5
|
def build_arel
|
7
6
|
select_by_tableoid = select_values.delete(:tableoid) == :tableoid
|
@@ -21,28 +20,16 @@ module ActiveRecord
|
|
21
20
|
|
22
21
|
def tableoid?(klass)
|
23
22
|
!Thread.current['skip_tableoid_cast'] &&
|
24
|
-
|
25
|
-
|
26
|
-
end
|
27
|
-
|
28
|
-
def tableoid_project?(klass)
|
29
|
-
tableoid?(klass) &&
|
30
|
-
(group_values - [:tableoid]).any?
|
31
|
-
end
|
32
|
-
|
33
|
-
def tableoid_group?(klass)
|
34
|
-
tableoid?(klass) &&
|
35
|
-
group_values.any?
|
23
|
+
klass.using_multi_table_inheritance? &&
|
24
|
+
klass.has_tableoid_column?
|
36
25
|
end
|
37
26
|
|
38
27
|
def tableoid_project(klass)
|
39
|
-
|
40
|
-
# Arel::Nodes::NamedFunction.new('CAST', [@klass.arel_table['tableoid::regclass'].as('regclass')])
|
41
|
-
@klass.mti_type_column.as('tableoid')
|
28
|
+
klass.mti_type_column.as('tableoid')
|
42
29
|
end
|
43
30
|
|
44
31
|
def tableoid_group(klass)
|
45
|
-
|
32
|
+
klass.mti_type_column
|
46
33
|
end
|
47
34
|
|
48
35
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module MTI
|
3
|
+
module Registry
|
4
|
+
|
5
|
+
mattr_accessor :tableoids
|
6
|
+
self.tableoids = { ActiveRecord::Base => false }
|
7
|
+
|
8
|
+
def self.[]=(klass, tableoid)
|
9
|
+
ActiveRecord::MTI.logger.debug "Adding #{klass} to MTI list with #{tableoid}"
|
10
|
+
tableoids[klass] = tableoid
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.find_mti_class(tableoid)
|
14
|
+
tableoids.key(tableoid)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.tableoid?(klass)
|
18
|
+
tableoids[klass]
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -1,5 +1,3 @@
|
|
1
|
-
require 'active_support/concern'
|
2
|
-
|
3
1
|
# Modified SchemaDumper that knows how to dump
|
4
2
|
# inherited tables. Key is that we have to dump parent
|
5
3
|
# tables before we dump child tables (of course).
|
@@ -12,169 +10,68 @@ module ActiveRecord
|
|
12
10
|
# output format (i.e., ActiveRecord::Schema).
|
13
11
|
module MTI
|
14
12
|
module SchemaDumper #:nodoc:
|
15
|
-
extend ActiveSupport::Concern
|
16
|
-
|
17
|
-
|
18
|
-
included do
|
19
|
-
|
20
|
-
private
|
21
|
-
|
22
|
-
def dumped_tables
|
23
|
-
@dumped_tables ||= []
|
24
|
-
end
|
25
|
-
|
26
|
-
# Output table and columns - but don't output columns that are inherited from
|
27
|
-
# a parent table.
|
28
|
-
#
|
29
|
-
# TODO: Qualify with the schema name IF the table is in a schema other than the first
|
30
|
-
# schema in the search path (not including the $user schema)
|
31
|
-
def table(table, stream)
|
32
|
-
return if already_dumped?(table)
|
33
|
-
|
34
|
-
if parent_table = @connection.parent_table(table)
|
35
|
-
table(parent_table, stream)
|
36
|
-
parent_column_names = @connection.columns(parent_table).map(&:name)
|
37
|
-
end
|
38
|
-
|
39
|
-
columns = @connection.columns(table)
|
40
|
-
begin
|
41
|
-
tbl = StringIO.new
|
42
|
-
|
43
|
-
# first dump primary key column
|
44
|
-
pk = @connection.primary_key(table)
|
45
|
-
|
46
|
-
tbl.print " create_table #{remove_prefix_and_suffix(table).inspect}"
|
47
|
-
if parent_table
|
48
|
-
tbl.print %Q(, inherits: '#{parent_table}')
|
49
|
-
else
|
50
|
-
pkcol = columns.detect { |c| c.name == pk }
|
51
|
-
if pkcol
|
52
|
-
if pk != 'id'
|
53
|
-
tbl.print %Q(, primary_key: '#{pk}')
|
54
|
-
elsif pkcol.sql_type == 'bigint'
|
55
|
-
tbl.print ", id: :bigserial"
|
56
|
-
elsif pkcol.sql_type == 'uuid'
|
57
|
-
tbl.print ", id: :uuid"
|
58
|
-
tbl.print %Q(, default: #{pkcol.default_function.inspect})
|
59
|
-
end
|
60
|
-
else
|
61
|
-
tbl.print ", id: false"
|
62
|
-
end
|
63
|
-
tbl.print ", force: :cascade"
|
64
|
-
end
|
65
|
-
tbl.puts " do |t|"
|
66
|
-
|
67
|
-
# then dump all non-primary key columns
|
68
|
-
column_specs = columns.map do |column|
|
69
|
-
raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" unless @connection.valid_type?(column.type)
|
70
|
-
next if column.name == pk
|
71
|
-
|
72
|
-
# Except columns in parent table
|
73
|
-
next if parent_column_names && parent_column_names.include?(column.name)
|
74
|
-
|
75
|
-
@connection.column_spec(column, @types)
|
76
|
-
end.compact
|
77
|
-
|
78
|
-
# find all migration keys used in this table
|
79
|
-
keys = @connection.migration_keys
|
80
|
-
|
81
|
-
# figure out the lengths for each column based on above keys
|
82
|
-
lengths = keys.map { |key|
|
83
|
-
column_specs.map { |spec|
|
84
|
-
spec[key] ? spec[key].length + 2 : 0
|
85
|
-
}.max
|
86
|
-
}
|
87
|
-
|
88
|
-
# the string we're going to sprintf our values against, with standardized column widths
|
89
|
-
format_string = lengths.map{ |len| "%-#{len}s" }
|
90
|
-
|
91
|
-
# find the max length for the 'type' column, which is special
|
92
|
-
type_length = column_specs.map{ |column| column[:type].length }.max
|
93
13
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
format_string *= ''
|
14
|
+
def dumped_tables
|
15
|
+
@dumped_tables ||= []
|
16
|
+
end
|
98
17
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
tbl.puts
|
104
|
-
end
|
18
|
+
# Output table and columns - but don't output columns that are inherited from
|
19
|
+
# a parent table.
|
20
|
+
def table(table, stream)
|
21
|
+
return if already_dumped?(table)
|
105
22
|
|
106
|
-
|
107
|
-
|
23
|
+
new_stream = StringIO.new
|
24
|
+
super(table, new_stream)
|
25
|
+
string = new_stream.string
|
108
26
|
|
109
|
-
|
27
|
+
if (parent_table = @connection.parent_table(table))
|
28
|
+
table(parent_table, stream)
|
29
|
+
string = inject_inherits_for_create_table(string, table, parent_table)
|
30
|
+
string = remove_parent_table_columns(string, @connection.columns(parent_table))
|
110
31
|
|
111
|
-
|
112
|
-
|
113
|
-
rescue => e
|
114
|
-
stream.puts "# Could not dump table #{table.inspect} because of following #{e.class}"
|
115
|
-
stream.puts "# #{e.message}"
|
116
|
-
stream.puts
|
117
|
-
end
|
32
|
+
pindexes = Hash[@connection.indexes(parent_table).map { |index| [index.columns, index] }]
|
33
|
+
cindexes = Hash[@connection.indexes(table).map { |index| [index.columns, index] }]
|
118
34
|
|
119
|
-
|
120
|
-
stream
|
35
|
+
string = remove_parent_table_indexes(string, (pindexes & cindexes).values)
|
121
36
|
end
|
122
37
|
|
123
|
-
#
|
124
|
-
|
125
|
-
def indexes(table, stream)
|
126
|
-
if (indexes = @connection.indexes(table)).any?
|
127
|
-
if parent_table = @connection.parent_table(table)
|
128
|
-
parent_indexes = @connection.indexes(parent_table)
|
129
|
-
end
|
130
|
-
|
131
|
-
indexes.delete_if {|i| is_parent_index?(i, parent_indexes) } if parent_indexes
|
132
|
-
return if indexes.empty?
|
133
|
-
|
134
|
-
add_index_statements = indexes.map do |index|
|
135
|
-
statement_parts = [
|
136
|
-
('add_index ' + remove_prefix_and_suffix(index.table).inspect),
|
137
|
-
index.columns.inspect,
|
138
|
-
('name: ' + index.name.inspect),
|
139
|
-
]
|
140
|
-
statement_parts << 'unique: true' if index.unique
|
141
|
-
|
142
|
-
index_lengths = (index.lengths || []).compact
|
143
|
-
statement_parts << ('length: ' + Hash[index.columns.zip(index.lengths)].inspect) unless index_lengths.empty?
|
38
|
+
# We've done this table
|
39
|
+
dumped_tables << table
|
144
40
|
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
statement_parts << ('where: ' + index.where.inspect) if index.where
|
149
|
-
|
150
|
-
statement_parts << ('using: ' + index.using.inspect) if index.using
|
151
|
-
|
152
|
-
statement_parts << ('type: ' + index.type.inspect) if index.type
|
41
|
+
stream.write string
|
42
|
+
stream
|
43
|
+
end
|
153
44
|
|
154
|
-
|
155
|
-
|
45
|
+
def inject_inherits_for_create_table(string, table, parent_table)
|
46
|
+
tbl_start = "create_table #{remove_prefix_and_suffix(table).inspect}"
|
47
|
+
tbl_end = " do |t|"
|
48
|
+
tbl_inherit = ", inherits: '#{parent_table}'"
|
49
|
+
string.gsub!(/#{Regexp.escape(tbl_start)}.*#{Regexp.escape(tbl_end)}/, tbl_start + tbl_inherit + tbl_end)
|
50
|
+
end
|
156
51
|
|
157
|
-
|
158
|
-
|
159
|
-
|
52
|
+
def remove_parent_table_columns(string, columns)
|
53
|
+
columns.each do |col|
|
54
|
+
string.gsub!(/\s+t\.\w+\s+("|')#{col.name}("|').*/, '')
|
160
55
|
end
|
56
|
+
string
|
57
|
+
end
|
161
58
|
|
162
|
-
|
163
|
-
|
164
|
-
|
59
|
+
def remove_parent_table_indexes(string, indexes)
|
60
|
+
indexes.each do |index|
|
61
|
+
string.gsub!(/\s*add_index .*name: #{Regexp.escape(index.name.inspect)}.*/, '') # Rails 4.x
|
62
|
+
string.gsub!(/\s+t\.index.*("|')#{index.name}("|').*/, '') # Rails 5.x
|
165
63
|
end
|
64
|
+
string
|
65
|
+
end
|
166
66
|
|
167
|
-
|
168
|
-
|
169
|
-
|
67
|
+
def remove_prefix_and_suffix(table)
|
68
|
+
table.gsub(/^(#{ActiveRecord::Base.table_name_prefix})(.+)(#{ActiveRecord::Base.table_name_suffix})$/, "\\2")
|
69
|
+
end
|
170
70
|
|
171
|
-
|
172
|
-
|
173
|
-
return true if pindex.columns == index.columns
|
174
|
-
end
|
175
|
-
return false
|
176
|
-
end
|
71
|
+
def already_dumped?(table)
|
72
|
+
dumped_tables.include? table
|
177
73
|
end
|
74
|
+
|
178
75
|
end
|
179
76
|
end
|
180
77
|
end
|
data/lib/active_record/mti.rb
CHANGED
@@ -1,7 +1,14 @@
|
|
1
1
|
require 'active_record/mti/version'
|
2
|
+
|
3
|
+
require 'active_support/all'
|
4
|
+
|
2
5
|
require 'active_record'
|
3
6
|
require 'active_record/connection_handling'
|
7
|
+
|
8
|
+
require 'core_ext/hash'
|
9
|
+
|
4
10
|
require 'active_record/mti/schema_dumper'
|
11
|
+
require 'active_record/mti/registry'
|
5
12
|
require 'active_record/mti/inheritance'
|
6
13
|
require 'active_record/mti/model_schema'
|
7
14
|
require 'active_record/mti/query_methods'
|
@@ -14,6 +21,24 @@ require 'active_record/mti/railtie' if defined?(Rails::Railtie)
|
|
14
21
|
module ActiveRecord
|
15
22
|
module MTI
|
16
23
|
|
24
|
+
# Rails likes to make breaking changes in it's minor versions (like 4.1 - 4.2) :P
|
25
|
+
mattr_accessor :oid_class
|
26
|
+
|
27
|
+
# Cannot assign default inside block because of rails 4.0
|
28
|
+
self.oid_class =
|
29
|
+
[
|
30
|
+
'::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::OID::Integer', # 4.0, 4.1
|
31
|
+
'::ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Integer', # 4.2
|
32
|
+
'::ActiveRecord::Type::Integer' # 5.0, 5.1
|
33
|
+
].find(nil) { |klass|
|
34
|
+
begin
|
35
|
+
klass.constantize
|
36
|
+
true
|
37
|
+
rescue NameError
|
38
|
+
false
|
39
|
+
end
|
40
|
+
}.constantize
|
41
|
+
|
17
42
|
class << self
|
18
43
|
attr_writer :logger
|
19
44
|
|
@@ -30,13 +55,13 @@ module ActiveRecord
|
|
30
55
|
end
|
31
56
|
|
32
57
|
def self.load
|
33
|
-
::ActiveRecord::Base.send :
|
34
|
-
::ActiveRecord::Base.send :
|
58
|
+
::ActiveRecord::Base.send :prepend, ModelSchema
|
59
|
+
::ActiveRecord::Base.send :prepend, Inheritance
|
35
60
|
::ActiveRecord::Relation.send :prepend, QueryMethods
|
36
61
|
::ActiveRecord::Relation.send :prepend, Calculations
|
37
62
|
::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send :prepend, ConnectionAdapters::PostgreSQL::Adapter
|
38
|
-
::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send :
|
39
|
-
::ActiveRecord::SchemaDumper.send :
|
63
|
+
::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send :prepend, ConnectionAdapters::PostgreSQL::SchemaStatements
|
64
|
+
::ActiveRecord::SchemaDumper.send :prepend, SchemaDumper
|
40
65
|
end
|
41
66
|
|
42
67
|
def self.testify(value)
|
@@ -2,28 +2,33 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe ActiveRecord::MTI::Inheritance do
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
5
|
+
it "creates a column even if class doesn't respond to :attribute" do
|
6
|
+
allow(Admin).to receive(:respond_to?).with(:attribute).and_return(false)
|
7
|
+
|
8
|
+
ActiveRecord::MTI::Registry.tableoids[Admin] = nil
|
9
|
+
|
10
|
+
expect(Admin.using_multi_table_inheritance?).to eq(true)
|
8
11
|
end
|
9
12
|
|
10
13
|
context 'class definition' do
|
11
14
|
|
12
15
|
describe 'for classes that use MTI' do
|
13
16
|
it "doesn't check inheritance multiple times" do
|
14
|
-
|
15
|
-
|
17
|
+
# Due to the anonymous class ("god = Class.new(Admin)") rspec can't properly distinquish
|
18
|
+
# between the two classes. So at most 2 times!
|
19
|
+
expect(Admin).to receive(:check_inheritance_of).and_call_original.at_most(2).times
|
16
20
|
|
17
21
|
Admin.create(email: 'foo@bar.baz', god_powers: 3)
|
18
22
|
Admin.create(email: 'foo2@bar.baz', god_powers: 3)
|
19
23
|
Admin.create(email: 'foo24@bar.baz', god_powers: 3)
|
24
|
+
Admin.create(email: 'foo246@bar.baz', god_powers: 3)
|
20
25
|
|
21
26
|
end
|
22
27
|
end
|
23
28
|
|
24
29
|
describe "for classes that don't use MTI" do
|
25
30
|
it "doesn't check inheritance multiple times" do
|
26
|
-
|
31
|
+
# ActiveRecord::MTI::Inheritance.register(Post, false)
|
27
32
|
expect(Post).to receive(:check_inheritance_of).and_call_original.exactly(1).times
|
28
33
|
|
29
34
|
Post.create(title: 'foo@bar.baz')
|
@@ -73,19 +78,34 @@ describe ActiveRecord::MTI::Inheritance do
|
|
73
78
|
god = Class.new(Admin)
|
74
79
|
expect(god.table_name).to eql(Admin.table_name)
|
75
80
|
end
|
81
|
+
|
82
|
+
it 'infers the table_name when defined dynamically' do
|
83
|
+
|
84
|
+
class Scrub < ActiveRecord::Base
|
85
|
+
const_set(:All, Class.new(Scrub) do |klass|
|
86
|
+
class_eval <<-AAA
|
87
|
+
self.table_name = 'scrubs/all'
|
88
|
+
AAA
|
89
|
+
end)
|
90
|
+
end
|
91
|
+
|
92
|
+
expect(Scrub::All.table_name).to eq('scrubs/all')
|
93
|
+
end
|
76
94
|
end
|
77
95
|
end
|
78
96
|
|
79
97
|
describe 'views' do
|
80
98
|
before(:each) do
|
99
|
+
|
100
|
+
User.connection.execute <<-SQL
|
101
|
+
CREATE OR REPLACE VIEW "users_all"
|
102
|
+
AS SELECT * FROM "users"
|
103
|
+
SQL
|
104
|
+
|
81
105
|
class UserView < User
|
82
106
|
self.table_name = "users_all"
|
83
107
|
end
|
84
108
|
|
85
|
-
UserView.connection.execute <<-SQL
|
86
|
-
CREATE OR REPLACE VIEW "users_all"
|
87
|
-
AS #{ User.all.to_sql }
|
88
|
-
SQL
|
89
109
|
end
|
90
110
|
|
91
111
|
if ActiveRecord::Base.connection.version >= Gem::Version.new('9.4')
|
@@ -99,10 +119,7 @@ describe ActiveRecord::MTI::Inheritance do
|
|
99
119
|
describe 'dynamic class creation' do
|
100
120
|
it 'infers the table_name from superclass not base_class' do
|
101
121
|
God = Class.new(Admin)
|
102
|
-
|
103
|
-
Hacker = Class.new(Admin) do
|
104
|
-
uses_mti
|
105
|
-
end
|
122
|
+
Hacker = Class.new(Admin)
|
106
123
|
|
107
124
|
expect(God.table_name).to eql(Admin.table_name)
|
108
125
|
expect(Hacker.table_name).to eql('admin/hackers')
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ActiveRecord::MTI::ModelSchema do
|
4
|
+
|
5
|
+
it 'rescues suffix' do
|
6
|
+
f = ActiveRecord::ModelSchema::ClassMethods
|
7
|
+
allow_any_instance_of(f).to receive(:full_table_name_suffix).and_raise(NoMethodError)
|
8
|
+
expect( Admin.full_table_name_suffix ).to eq("")
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ActiveRecord::MTI::SchemaDumper do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
ActiveRecord::SchemaMigration.create_table
|
7
|
+
end
|
8
|
+
|
9
|
+
let(:hacker_sql) {
|
10
|
+
<<-FOO
|
11
|
+
create_table "admin/hackers", inherits: 'admins' do |t|
|
12
|
+
end
|
13
|
+
FOO
|
14
|
+
}
|
15
|
+
|
16
|
+
it 'does not dump indexes for child table' do
|
17
|
+
stream = StringIO.new
|
18
|
+
ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
|
19
|
+
|
20
|
+
expect(stream.string).to include(hacker_sql)
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ActiveRecord::Inheritance do
|
4
|
+
|
5
|
+
context 'class definition' do
|
6
|
+
|
7
|
+
describe 'for classes that use STI' do
|
8
|
+
it "doesn't check inheritance multiple times" do
|
9
|
+
|
10
|
+
Transportation::Military::Vehicle.create(color: :red)
|
11
|
+
Transportation::Military::Vehicle.create(color: :blue)
|
12
|
+
Transportation::Military::Vehicle.create(color: :green)
|
13
|
+
Transportation::Military::Vehicle.create(color: :gold)
|
14
|
+
|
15
|
+
vehicle = Transportation::Military::Vehicle.first
|
16
|
+
expect(vehicle.class.name).to eq('Transportation::Military::Vehicle')
|
17
|
+
expect(vehicle.color).to eq('red')
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
data/spec/schema.rb
CHANGED
data/spec/spec_helper.rb
CHANGED
@@ -25,7 +25,7 @@ ActiveRecord::Base.establish_connection db_config
|
|
25
25
|
|
26
26
|
load File.dirname(__FILE__) + '/schema.rb'
|
27
27
|
|
28
|
-
Dir[
|
28
|
+
Dir[ActiveRecord::MTI.root.join('spec', 'support', '**', '**.rb')].each do |f|
|
29
29
|
require f
|
30
30
|
end
|
31
31
|
|
data/spec/support/models.rb
CHANGED
@@ -12,7 +12,6 @@ class Post < ::ActiveRecord::Base
|
|
12
12
|
end
|
13
13
|
|
14
14
|
class User < ::ActiveRecord::Base
|
15
|
-
uses_mti
|
16
15
|
|
17
16
|
has_many :posts
|
18
17
|
has_many :comments
|
@@ -25,10 +24,15 @@ end
|
|
25
24
|
|
26
25
|
module Transportation
|
27
26
|
class Vehicle < ::ActiveRecord::Base
|
28
|
-
uses_mti
|
29
27
|
end
|
30
28
|
|
31
29
|
class Truck < Vehicle
|
32
30
|
self.table_name = 'vehicles/trucks'
|
33
31
|
end
|
32
|
+
|
33
|
+
module Military
|
34
|
+
class Vehicle < ::Transportation::Vehicle
|
35
|
+
self.inheritance_column = 'type'
|
36
|
+
end
|
37
|
+
end
|
34
38
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_record-mti
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.0.pre.
|
4
|
+
version: 0.3.0.pre.rc2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dale Stevens
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-11-
|
11
|
+
date: 2017-11-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: pg
|
@@ -116,11 +116,16 @@ files:
|
|
116
116
|
- lib/active_record/mti/query_methods.rb
|
117
117
|
- lib/active_record/mti/querying.rb
|
118
118
|
- lib/active_record/mti/railtie.rb
|
119
|
+
- lib/active_record/mti/registry.rb
|
119
120
|
- lib/active_record/mti/schema_dumper.rb
|
120
121
|
- lib/active_record/mti/version.rb
|
122
|
+
- lib/core_ext/hash.rb
|
121
123
|
- spec/active_record/calculations_spec.rb
|
122
|
-
- spec/active_record/mti/
|
124
|
+
- spec/active_record/mti/inheritance_spec.rb
|
125
|
+
- spec/active_record/mti/model_schema_spec.rb
|
126
|
+
- spec/active_record/mti/schema_dumper_spec.rb
|
123
127
|
- spec/active_record/mti_spec.rb
|
128
|
+
- spec/active_record/sti/inheritance_spec.rb
|
124
129
|
- spec/schema.rb
|
125
130
|
- spec/spec_helper.rb
|
126
131
|
- spec/support/models.rb
|
@@ -146,14 +151,17 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
146
151
|
version: 1.3.1
|
147
152
|
requirements: []
|
148
153
|
rubyforge_project:
|
149
|
-
rubygems_version: 2.6.
|
154
|
+
rubygems_version: 2.6.13
|
150
155
|
signing_key:
|
151
156
|
specification_version: 4
|
152
157
|
summary: Multi Table Inheritance for PostgreSQL in Rails
|
153
158
|
test_files:
|
154
159
|
- spec/active_record/calculations_spec.rb
|
155
|
-
- spec/active_record/mti/
|
160
|
+
- spec/active_record/mti/inheritance_spec.rb
|
161
|
+
- spec/active_record/mti/model_schema_spec.rb
|
162
|
+
- spec/active_record/mti/schema_dumper_spec.rb
|
156
163
|
- spec/active_record/mti_spec.rb
|
164
|
+
- spec/active_record/sti/inheritance_spec.rb
|
157
165
|
- spec/schema.rb
|
158
166
|
- spec/spec_helper.rb
|
159
167
|
- spec/support/models.rb
|