activefacts-rmap 1.7.1
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 +9 -0
- data/.rspec +1 -0
- data/.travis.yml +4 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +32 -0
- data/Rakefile +6 -0
- data/activefacts-rmap.gemspec +25 -0
- data/lib/activefacts/rmap.rb +15 -0
- data/lib/activefacts/rmap/columns.rb +444 -0
- data/lib/activefacts/rmap/foreignkey.rb +187 -0
- data/lib/activefacts/rmap/index.rb +237 -0
- data/lib/activefacts/rmap/object_type.rb +198 -0
- data/lib/activefacts/rmap/reference.rb +433 -0
- data/lib/activefacts/rmap/tables.rb +380 -0
- data/lib/activefacts/rmap/version.rb +5 -0
- metadata +116 -0
@@ -0,0 +1,187 @@
|
|
1
|
+
#
|
2
|
+
# ActiveFacts Relational mapping
|
3
|
+
# A ForeignKey exists for every Reference from a ObjectType to another ObjectType that's a table.
|
4
|
+
#
|
5
|
+
# Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
|
6
|
+
#
|
7
|
+
module ActiveFacts
|
8
|
+
module RMap
|
9
|
+
class ForeignKey
|
10
|
+
# What table (ObjectType) is the FK from?
|
11
|
+
def from; @from; end
|
12
|
+
|
13
|
+
# What table (ObjectType) is the FK to?
|
14
|
+
def to; @to; end
|
15
|
+
|
16
|
+
# What reference created the FK?
|
17
|
+
def references; @references; end
|
18
|
+
|
19
|
+
# What columns in the *from* table form the FK
|
20
|
+
def from_columns; @from_columns; end
|
21
|
+
|
22
|
+
# What columns in the *to* table form the identifier
|
23
|
+
def to_columns; @to_columns; end
|
24
|
+
|
25
|
+
def initialize(from, to, references, from_columns, to_columns) #:nodoc:
|
26
|
+
@from, @to, @references, @from_columns, @to_columns =
|
27
|
+
from, to, references, from_columns, to_columns
|
28
|
+
end
|
29
|
+
|
30
|
+
def describe
|
31
|
+
"foreign key from #{from.name}(#{from_columns.map{|c| c.name}*', '}) to #{to.name}(#{to_columns.map{|c| c.name}*', '})"
|
32
|
+
end
|
33
|
+
|
34
|
+
def verbalised_path reverse = false
|
35
|
+
# REVISIT: This should be a proper join path verbalisation:
|
36
|
+
refs = reverse ? references.reverse : references
|
37
|
+
refs.map do |r|
|
38
|
+
r.verbalised_path reverse
|
39
|
+
end * ' and '
|
40
|
+
end
|
41
|
+
|
42
|
+
# Which references are absorbed into the "from" table?
|
43
|
+
def precursor_references
|
44
|
+
fk_jump = @references.detect(&:fk_jump)
|
45
|
+
jump_index = @references.index(fk_jump)
|
46
|
+
@references[0, jump_index]
|
47
|
+
end
|
48
|
+
|
49
|
+
# Which references are absorbed into the "to" table?
|
50
|
+
def following_references
|
51
|
+
fk_jump = @references.detect(&:fk_jump)
|
52
|
+
jump_index = @references.index(fk_jump)
|
53
|
+
fk_jump != @references.last ? @references[jump_index+1..-1] : []
|
54
|
+
end
|
55
|
+
|
56
|
+
def jump_reference
|
57
|
+
@references.detect(&:fk_jump)
|
58
|
+
end
|
59
|
+
|
60
|
+
def to_name
|
61
|
+
p = precursor_references
|
62
|
+
f = following_references
|
63
|
+
j = jump_reference
|
64
|
+
|
65
|
+
@references.last.to_names +
|
66
|
+
(p.empty? && f.empty? ? [] : ['via'] + p.map{|r| r.to_names}.flatten + f.map{|r| r.from_names}.flatten)
|
67
|
+
end
|
68
|
+
|
69
|
+
# The from_name is the role name of the table with the FK, viewed from the other end
|
70
|
+
# When there are no precursor_references or following_references, it's the jump_reference.from_names
|
71
|
+
# REVISIT: I'm still working out what to do with precursor_references and following_references
|
72
|
+
def from_name
|
73
|
+
p = precursor_references
|
74
|
+
f = following_references
|
75
|
+
j = jump_reference
|
76
|
+
|
77
|
+
# pluralise unless j.is_one_to_one
|
78
|
+
|
79
|
+
# REVISIT: references[0].from_names is where the FK lives; but the object of interest may be an absorbed subclass which we should use here instead:
|
80
|
+
# REVISIT: Should crunch superclasses in subtype traversals
|
81
|
+
# REVISIT: Need to add "_as_rolename" where rolename is not to.name
|
82
|
+
|
83
|
+
[
|
84
|
+
@references[0].from_names,
|
85
|
+
(p.empty? && f.empty? ? [] : ['via'] + p.map{|r| r.to_names}.flatten + f.map{|r| r.from_names}.flatten)
|
86
|
+
]
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
module Metamodel #:nodoc:
|
93
|
+
class ObjectType
|
94
|
+
# When an EntityType is fully absorbed, its foreign keys are too.
|
95
|
+
# Return an Array of Reference paths for such absorbed FKs
|
96
|
+
def all_absorbed_foreign_key_reference_path
|
97
|
+
references_from.inject([]) do |array, ref|
|
98
|
+
if ref.is_simple_reference
|
99
|
+
if TypeInheritance === ref.fact_type
|
100
|
+
# Ignore references to secondary supertypes, when absorption is through primary.
|
101
|
+
next array if absorbed_via && TypeInheritance === absorbed_via.fact_type
|
102
|
+
# Ignore the case where a subtype is absorbed elsewhere:
|
103
|
+
# REVISIT: Disabled, as this should never happen.
|
104
|
+
# next array if ref.to.absorbed_via != ref.fact_type
|
105
|
+
end
|
106
|
+
ref.fk_jump = true
|
107
|
+
array << [ref]
|
108
|
+
elsif ref.is_absorbing or (ref.to && !ref.to.is_table)
|
109
|
+
trace :fk, "getting fks absorbed into #{name} via #{ref}" do
|
110
|
+
ref.to.all_absorbed_foreign_key_reference_path.each do |aref|
|
111
|
+
array << aref.insert(0, ref)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
array
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def foreign_keys_to
|
120
|
+
@foreign_keys_to ||= []
|
121
|
+
end
|
122
|
+
|
123
|
+
# Return an array of all the foreign keys from this table
|
124
|
+
def foreign_keys
|
125
|
+
|
126
|
+
# Get the ForeignKey object for each absorbed reference path
|
127
|
+
@foreign_keys ||=
|
128
|
+
begin
|
129
|
+
fk_ref_paths = all_absorbed_foreign_key_reference_path
|
130
|
+
fk_ref_paths.map do |fk_ref_path|
|
131
|
+
trace :fk, "\nFK: " + fk_ref_path.map{|fk_ref| fk_ref.reading }*" and " do
|
132
|
+
|
133
|
+
from_columns = (columns||all_columns({})).select{|column|
|
134
|
+
column.references[0...fk_ref_path.size] == fk_ref_path
|
135
|
+
}
|
136
|
+
trace :fk, "from_columns = #{from_columns.map { |column| column.name }*", "}"
|
137
|
+
|
138
|
+
# Figure out absorption on the target end:
|
139
|
+
to = fk_ref_path.last.to
|
140
|
+
if to.absorbed_via
|
141
|
+
trace :fk, "Reference target #{fk_ref_path.last.to.name} is absorbed via:" do
|
142
|
+
while (r = to.absorbed_via)
|
143
|
+
m = r.reversed
|
144
|
+
trace :fk, "#{m.reading}"
|
145
|
+
fk_ref_path << m
|
146
|
+
to = m.from == to ? m.to : m.from
|
147
|
+
end
|
148
|
+
trace :fk, "Absorption ends at #{to.name}"
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# REVISIT: This test may no longer be necessary
|
153
|
+
raise "REVISIT: #{fk_ref_path.inspect} is bad" unless to and to.columns
|
154
|
+
|
155
|
+
# REVISIT: This fails for absorbed subtypes having their own identification.
|
156
|
+
# Check the CompanyDirectorEmployee model for example, EmployeeManagerNr -> Person (should reference EmployeeNr)
|
157
|
+
# Need to use the absorbed identifier_columns of the subtype,
|
158
|
+
# not the columns of the supertype that absorbs it.
|
159
|
+
# But in general, that isn't going to work because in most DBMS
|
160
|
+
# there's no suitable uniquen index on the subtype's identifier_columns
|
161
|
+
|
162
|
+
to_columns = fk_ref_path[-1].to.identifier_columns
|
163
|
+
|
164
|
+
# Put the column pairs in the correct order. They MUST be in the order they appear in the primary key
|
165
|
+
froms, tos = from_columns.zip(to_columns).sort_by { |pair|
|
166
|
+
to_columns.index(pair[1])
|
167
|
+
}.transpose
|
168
|
+
|
169
|
+
fk = ActiveFacts::RMap::ForeignKey.new(self, to, fk_ref_path, froms, tos)
|
170
|
+
to.foreign_keys_to << fk
|
171
|
+
fk
|
172
|
+
end
|
173
|
+
end.
|
174
|
+
sort_by do |fk|
|
175
|
+
# Put the foreign keys in a defined order:
|
176
|
+
# debugger if !fk.to_columns || fk.to_columns.include?(nil) || !fk.from_columns || fk.from_columns.include?(nil)
|
177
|
+
[ fk.to.name,
|
178
|
+
fk.to_columns.map{|col| col.name(nil).sort},
|
179
|
+
fk.from_columns.map{|col| col.name(nil).sort}
|
180
|
+
]
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
@@ -0,0 +1,237 @@
|
|
1
|
+
#
|
2
|
+
# ActiveFacts Relational mapping
|
3
|
+
# An Index on a ObjectType is used to represent a unique constraint across roles absorbed
|
4
|
+
# into that object_type's table.
|
5
|
+
#
|
6
|
+
# Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
|
7
|
+
#
|
8
|
+
|
9
|
+
module ActiveFacts
|
10
|
+
module RMap
|
11
|
+
class Index
|
12
|
+
# The UniquenessConstraint that created this index
|
13
|
+
def uniqueness_constraint; @uniqueness_constraint; end
|
14
|
+
|
15
|
+
# The table that the index is on
|
16
|
+
def on; @on; end
|
17
|
+
|
18
|
+
# If a non-mandatory reference was absorbed, only the non-nil instances are unique.
|
19
|
+
# Return the ObjectType that was absorbed, which might differ from this Index's table.
|
20
|
+
def over; @over; end
|
21
|
+
|
22
|
+
# Return the array of columns in this index
|
23
|
+
def columns; @columns; end
|
24
|
+
|
25
|
+
# Is this index the primary key for this table?
|
26
|
+
def is_primary; @is_primary; end
|
27
|
+
|
28
|
+
# Is this index unique?
|
29
|
+
def is_unique; @is_unique; end
|
30
|
+
|
31
|
+
# An Index arises from a uniqueness constraint and applies to a table,
|
32
|
+
# but because the UC may actually be over an object absorbed into the table,
|
33
|
+
# we must record that object also.
|
34
|
+
# We record the columns it's over, whether it's primary (for 'over'),
|
35
|
+
# and whether it's unique (always, at present)
|
36
|
+
def initialize(uc, on, over, columns, is_primary, is_unique = true) #:nodoc:
|
37
|
+
@uniqueness_constraint, @on, @over, @columns, @is_primary, @is_unique =
|
38
|
+
uc, on, over, columns, is_primary, is_unique
|
39
|
+
end
|
40
|
+
|
41
|
+
# The name that was assigned (perhaps implicitly by NORMA)
|
42
|
+
def real_name
|
43
|
+
@uniqueness_constraint.name && @uniqueness_constraint.name != '' ? @uniqueness_constraint.name.gsub(' ','') : nil
|
44
|
+
end
|
45
|
+
|
46
|
+
# This name is either the name explicitly assigned (if any) or is constructed to form a unique index name.
|
47
|
+
def name
|
48
|
+
uc = @uniqueness_constraint
|
49
|
+
r = real_name
|
50
|
+
return r if r && r !~ /^(Ex|In)ternalUniquenessConstraint[0-9]+$/
|
51
|
+
(uc.is_preferred_identifier ? "PK_" : "IX_") +
|
52
|
+
view_name +
|
53
|
+
(uc.is_preferred_identifier ? "" : "By"+column_names*"")
|
54
|
+
end
|
55
|
+
|
56
|
+
# An array of the names of the columns this index covers
|
57
|
+
def column_names(separator = "")
|
58
|
+
columns.map{|column| column.name(separator)}
|
59
|
+
end
|
60
|
+
|
61
|
+
# An array of the names of the columns this index covers, with some lexical truncations.
|
62
|
+
def abbreviated_column_names(separator = "")
|
63
|
+
columns.map{|column| column.name(separator).sub(/^#{over.name}/,'')}
|
64
|
+
end
|
65
|
+
|
66
|
+
# The name of a view that can be created to enforce uniqueness over non-null key values
|
67
|
+
def view_name
|
68
|
+
"#{over.name.gsub(' ','')}#{on == over ? "" : "In"+on.name.gsub(' ','')}"
|
69
|
+
end
|
70
|
+
|
71
|
+
def to_s #:nodoc:
|
72
|
+
if @uniqueness_constraint
|
73
|
+
name = @uniqueness_constraint.name
|
74
|
+
preferred = @uniqueness_constraint.is_preferred_identifier ? " (preferred)" : ""
|
75
|
+
else
|
76
|
+
name = "#{@on.name}IsUnique"
|
77
|
+
preferred = !@on.injected_surrogate_role ? " (preferred)" : ""
|
78
|
+
end
|
79
|
+
colnames = @columns.map(&:name)*", "
|
80
|
+
"Index #{name} on #{@on.name} over #{@over.name}(#{colnames})#{preferred}"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
module Metamodel #:nodoc:
|
86
|
+
class EntityType
|
87
|
+
def self_index
|
88
|
+
nil
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
class ValueType
|
93
|
+
def self_index
|
94
|
+
ActiveFacts::RMap::Index.new(
|
95
|
+
nil, # The implied uniqueness constraint is not created
|
96
|
+
self, # ValueType being indexed
|
97
|
+
self, # Absorbed object being indexed
|
98
|
+
columns.select{|c| c.references[0].is_self_value},
|
99
|
+
injected_surrogate_role ? false : true
|
100
|
+
)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
class ObjectType
|
105
|
+
# An array of each Index for this table
|
106
|
+
def indices
|
107
|
+
@indices || populate_indices
|
108
|
+
end
|
109
|
+
|
110
|
+
def clear_indices #:nodoc:
|
111
|
+
# Clear any previous indices
|
112
|
+
@indices = nil
|
113
|
+
end
|
114
|
+
|
115
|
+
def populate_indices #:nodoc:
|
116
|
+
# The absorption path of a column indicates how it came to be in this table.
|
117
|
+
# It might be a direct many:one valuetype relationship, or it might be in such
|
118
|
+
# a relationship to an entity that was absorbed into this table (and so on).
|
119
|
+
# The reference path is the set of absorption references and one past it.
|
120
|
+
# Stopping here means we don't dig into the definitions of FK column counterparts.
|
121
|
+
# Note that many columns of an object may have the same ref_path.
|
122
|
+
#
|
123
|
+
# REVISIT:
|
124
|
+
# Note also that this produces columns ordered for each refpath the same as the
|
125
|
+
# order of the columns, not the same as the columns in the PK for which they might be an FK.
|
126
|
+
all_column_by_ref_path =
|
127
|
+
trace :index2, "Indexing columns by ref_path" do
|
128
|
+
columns.inject({}) do |hash, column|
|
129
|
+
trace :index2, "References in column #{name}.#{column.name}" do
|
130
|
+
ref_path = column.absorption_references
|
131
|
+
raise "No absorption_references for #{column.name} from #{column.references.map(&:to_s)*" and "}" if !ref_path || ref_path.empty?
|
132
|
+
(hash[ref_path] ||= []) << column
|
133
|
+
trace :index2, "#{column.name} involves #{ref_path.map(&:to_s)*" and "}"
|
134
|
+
end
|
135
|
+
hash
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
columns_by_unique_constraint = {}
|
140
|
+
all_column_by_role_ref =
|
141
|
+
all_column_by_ref_path.
|
142
|
+
keys. # Go through all refpaths and find uniqueness constraints
|
143
|
+
inject({}) do |hash, ref_path|
|
144
|
+
ref_path.each do |ref|
|
145
|
+
next unless ref.to_role
|
146
|
+
# trace :index2, "Considering #{ref_path.map(&:to_s)*" and "} yielding columns #{all_column_by_ref_path[ref_path].map{|c| c.name('.')}*", "}"
|
147
|
+
ref.to_role.all_role_ref.each do |role_ref|
|
148
|
+
all_pcs = role_ref.role_sequence.all_presence_constraint
|
149
|
+
# puts "pcs over #{ref_path.map{|r| r.to_names}.flatten*'.'}: #{role_ref.role_sequence.all_presence_constraint.map(&:describe)*"; "}" if all_pcs.size > 0
|
150
|
+
pcs = all_pcs.
|
151
|
+
reject do |pc|
|
152
|
+
!pc.max_frequency or # No maximum freq; cannot be a uniqueness constraint
|
153
|
+
pc.max_frequency != 1 or # maximum is not 1
|
154
|
+
# Constraint is not over a unary fact type role (NORMA does this)
|
155
|
+
pc.role_sequence.all_role_ref.size == 1 && ref_path[-1].to_role.fact_type.all_role.size == 1
|
156
|
+
end
|
157
|
+
next unless pcs.size > 0
|
158
|
+
# The columns for this ref_path support the UCs in "pcs".
|
159
|
+
pcs.each do |pc|
|
160
|
+
ref_columns = all_column_by_ref_path[ref_path]
|
161
|
+
ordinal = role_ref.ordinal # Position in priority order
|
162
|
+
ref_columns.each_with_index do |column, index|
|
163
|
+
#puts "Adding index column #{column.name} in rank[#{ordinal},#{index}]"
|
164
|
+
# REVISIT: the "index" here might be a duplicate in some cases: change sort_by below to just sort and run the SeparateSubtypes CQL model for example.
|
165
|
+
(columns_by_unique_constraint[pc] ||= []) << [ordinal, index, column]
|
166
|
+
end
|
167
|
+
end
|
168
|
+
hash[role_ref] = all_column_by_ref_path[ref_path]
|
169
|
+
end
|
170
|
+
end
|
171
|
+
hash
|
172
|
+
end
|
173
|
+
|
174
|
+
trace :index, "All Indices in #{name}:" do
|
175
|
+
@indices = columns_by_unique_constraint.map do |uc, columns_with_ordinal|
|
176
|
+
trace :index, "Index due to uc #{uc.concept.guid} on #{name} over (#{columns_with_ordinal.sort_by{|onc|onc[0]}.map{|ca| ca[2].name}.inspect})"
|
177
|
+
columns = columns_with_ordinal.sort_by{|ca| [ca[0,2], ca[2].name]}.map{|ca| ca[2]}
|
178
|
+
absorption_level = columns.map(&:absorption_level).min
|
179
|
+
over = columns[0].references[absorption_level].from
|
180
|
+
|
181
|
+
# Absorption through a one-to-one forms a UC that we don't need to enforce using an index:
|
182
|
+
if over != self and
|
183
|
+
over.absorbed_via == columns[0].references[absorption_level-1] and
|
184
|
+
(rr = uc.role_sequence.all_role_ref.single) and
|
185
|
+
over.absorbed_via.fact_type.all_role.include?(rr.role)
|
186
|
+
next nil
|
187
|
+
end
|
188
|
+
|
189
|
+
index = ActiveFacts::RMap::Index.new(
|
190
|
+
uc,
|
191
|
+
self,
|
192
|
+
over,
|
193
|
+
columns,
|
194
|
+
uc.is_preferred_identifier
|
195
|
+
)
|
196
|
+
trace :index, index
|
197
|
+
index
|
198
|
+
end.
|
199
|
+
compact.
|
200
|
+
sort_by do |index|
|
201
|
+
# Put the indices in a defined order:
|
202
|
+
index.columns.map(&:name)+['', index.over.name]
|
203
|
+
end
|
204
|
+
end
|
205
|
+
si = self_index
|
206
|
+
@indices.unshift(si) if si
|
207
|
+
@indices
|
208
|
+
end
|
209
|
+
|
210
|
+
end
|
211
|
+
|
212
|
+
class Vocabulary
|
213
|
+
def populate_all_indices #:nodoc:
|
214
|
+
trace :index, "Populating all object_type indices" do
|
215
|
+
all_object_type.each do |object_type|
|
216
|
+
object_type.clear_indices
|
217
|
+
end
|
218
|
+
tables.each do |object_type|
|
219
|
+
trace :index, "Populating indices for #{object_type.name}" do
|
220
|
+
object_type.populate_indices
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
trace :index, "Finished object_type indices" do
|
225
|
+
tables.each do |object_type|
|
226
|
+
trace :index?, "#{object_type.name}:" do
|
227
|
+
object_type.indices.each do |index|
|
228
|
+
trace :index, index
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
end
|
237
|
+
end
|
@@ -0,0 +1,198 @@
|
|
1
|
+
require 'activefacts/support'
|
2
|
+
|
3
|
+
module ActiveFacts
|
4
|
+
module API
|
5
|
+
module ObjectType
|
6
|
+
def table
|
7
|
+
@is_table = true
|
8
|
+
end
|
9
|
+
|
10
|
+
def is_table
|
11
|
+
@is_table
|
12
|
+
end
|
13
|
+
|
14
|
+
def columns
|
15
|
+
raise "This method is no longer in use"
|
16
|
+
=begin
|
17
|
+
return @columns if @columns
|
18
|
+
trace :rmap, "Calculating columns for #{basename}" do
|
19
|
+
@columns = (
|
20
|
+
if superclass.is_entity_type
|
21
|
+
# REVISIT: Need keys to secondary supertypes as well, but no duplicates.
|
22
|
+
trace :rmap, "Separate subtype has a foreign key to its supertype" do
|
23
|
+
superclass.__absorb([[superclass.basename]], self)
|
24
|
+
end
|
25
|
+
else
|
26
|
+
[]
|
27
|
+
end +
|
28
|
+
# Then absorb all normal roles:
|
29
|
+
roles.values.select do |role|
|
30
|
+
role.unique && !role.counterpart_unary_has_precedence
|
31
|
+
end.inject([]) do |columns, role|
|
32
|
+
rn = role.name.to_s.split(/_/)
|
33
|
+
trace :rmap, "Role #{rn*'.'}" do
|
34
|
+
columns += role.counterpart_object_type.__absorb([rn], role.counterpart)
|
35
|
+
end
|
36
|
+
end +
|
37
|
+
# And finally all absorbed subtypes:
|
38
|
+
subtypes.
|
39
|
+
select{|subtype| !subtype.is_table}. # Don't absorb separate subtypes
|
40
|
+
inject([]) do |columns, subtype|
|
41
|
+
# Pass self as 2nd param here, not a role, standing for the supertype role
|
42
|
+
subtype_name = subtype.basename
|
43
|
+
trace :rmap, "Absorbing subtype #{subtype_name}" do
|
44
|
+
columns += subtype.__absorb([[subtype_name]], self)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
).map do |col_names|
|
48
|
+
last = nil
|
49
|
+
col_names.flatten.map do |name|
|
50
|
+
name.downcase.sub(/^[a-z]/){|c| c.upcase}
|
51
|
+
end.
|
52
|
+
reject do |n|
|
53
|
+
# Remove sequential duplicates:
|
54
|
+
dup = last == n
|
55
|
+
last = n
|
56
|
+
dup
|
57
|
+
end*"."
|
58
|
+
end
|
59
|
+
end
|
60
|
+
=end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Return an array of the absorbed columns, using prefix for name truncation
|
64
|
+
def __absorb(prefix, except_role = nil)
|
65
|
+
# also considered a table if the superclass isn't excluded and is (transitively) a table
|
66
|
+
if !@is_table && (except_role == superclass || !is_table_subtype)
|
67
|
+
if is_entity_type
|
68
|
+
if (role = fully_absorbed) && role != except_role
|
69
|
+
# If this non-table is fully absorbed into another table (not our caller!)
|
70
|
+
# (another table plays its single identifying role), then absorb that role only.
|
71
|
+
# counterpart_object_type = role.counterpart_object_type
|
72
|
+
# This omission matches the one in columns.rb, see EntityType#reference_columns
|
73
|
+
# new_prefix = prefix + [role.name.to_s.split(/_/)]
|
74
|
+
trace :rmap, "Reference to #{role.name} (absorbed elsewhere)" do
|
75
|
+
role.counterpart_object_type.__absorb(prefix, role.counterpart)
|
76
|
+
end
|
77
|
+
else
|
78
|
+
# Not a table -> all roles are absorbed
|
79
|
+
roles.
|
80
|
+
values.
|
81
|
+
select do |role|
|
82
|
+
role.unique && role != except_role && !role.counterpart_unary_has_precedence
|
83
|
+
end.
|
84
|
+
inject([]) do |columns, role|
|
85
|
+
columns += __absorb_role(prefix, role)
|
86
|
+
end +
|
87
|
+
subtypes. # Absorb subtype roles too!
|
88
|
+
select{|subtype| !subtype.is_table}. # Don't absorb separate subtypes
|
89
|
+
inject([]) do |columns, subtype|
|
90
|
+
# Pass self as 2nd param here, not a role, standing for the supertype role
|
91
|
+
new_prefix = prefix[0..-2] + [[subtype.basename]]
|
92
|
+
trace :rmap, "Absorbed subtype #{subtype.basename}" do
|
93
|
+
columns += subtype.__absorb(new_prefix, self)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
else
|
98
|
+
[prefix]
|
99
|
+
end
|
100
|
+
else
|
101
|
+
# Create a foreign key to the table
|
102
|
+
if is_entity_type
|
103
|
+
ir = identifying_role_names.map{|role_name| roles(role_name) }
|
104
|
+
trace :rmap, "Reference to #{basename} with #{prefix.inspect}" do
|
105
|
+
ic = identifying_role_names.map{|role_name| role_name.to_s.split(/_/)}
|
106
|
+
ir.inject([]) do |columns, role|
|
107
|
+
columns += __absorb_role(prefix, role)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
else
|
111
|
+
# Reference to value type which is a table
|
112
|
+
col = prefix.clone
|
113
|
+
trace :rmap, "Self-value #{col[-1]}.Value"
|
114
|
+
col[-1] += ["Value"]
|
115
|
+
col
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def __absorb_role(prefix, role)
|
121
|
+
if prefix.size > 0 and
|
122
|
+
(c = role.owner).is_entity_type and
|
123
|
+
c.identifying_roles == [role] and
|
124
|
+
(irn = c.identifying_role_names).size == 1 and
|
125
|
+
(n = irn[0].to_s.split(/_/)).size > 1 and
|
126
|
+
(owner = role.owner.basename.snakecase.split(/_/)) and
|
127
|
+
n[0...owner.size] == owner
|
128
|
+
trace :rmap, "truncating transitive identifying role #{n.inspect}"
|
129
|
+
owner.size.times { n.shift }
|
130
|
+
new_prefix = prefix + [n]
|
131
|
+
elsif (c = role.counterpart_object_type).is_entity_type and
|
132
|
+
(irn = c.identifying_role_names).size == 1 and
|
133
|
+
#irn[0].to_s.split(/_/)[0] == role.owner.basename.downcase
|
134
|
+
irn[0] == role.counterpart.name
|
135
|
+
#trace :rmap, "=== #{irn[0].to_s.split(/_/)[0]} elided ==="
|
136
|
+
new_prefix = prefix
|
137
|
+
elsif (fa_role = fully_absorbed) && fa_role == role
|
138
|
+
new_prefix = prefix
|
139
|
+
else
|
140
|
+
new_prefix = prefix + [role.name.to_s.split(/_/)]
|
141
|
+
end
|
142
|
+
#trace :rmap, "new_prefix is #{new_prefix*"."}"
|
143
|
+
|
144
|
+
trace :rmap, "Absorbing role #{role.name} as #{new_prefix[prefix.size..-1]*"."}" do
|
145
|
+
role.counterpart_object_type.__absorb(new_prefix, role.counterpart)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def is_table_subtype
|
150
|
+
return true if is_table
|
151
|
+
klass = superclass
|
152
|
+
while klass.is_entity_type
|
153
|
+
return true if klass.is_table
|
154
|
+
klass = klass.superclass
|
155
|
+
end
|
156
|
+
return false
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
module Entity
|
161
|
+
module ClassMethods
|
162
|
+
def fully_absorbed
|
163
|
+
return false unless (ir = identifying_role_names) && ir.size == 1
|
164
|
+
role = roles(ir[0])
|
165
|
+
return role if ((cp = role.counterpart_object_type).is_table ||
|
166
|
+
(cp.is_entity_type && cp.fully_absorbed))
|
167
|
+
return superclass if superclass.is_entity_type # Absorbed subtype
|
168
|
+
nil
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
# A one-to-one can be absorbed into either table. We decide which by comparing
|
174
|
+
# the names, just as happens in ObjectType.populate_reference (see reference.rb)
|
175
|
+
class Role
|
176
|
+
def counterpart_unary_has_precedence
|
177
|
+
counterpart_object_type.is_table_subtype and
|
178
|
+
counterpart.unique and
|
179
|
+
owner.name.downcase < counterpart.owner.name.downcase
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
class TrueClass
|
187
|
+
def self.__absorb(prefix, except_role = nil)
|
188
|
+
[prefix]
|
189
|
+
end
|
190
|
+
|
191
|
+
def self.is_table
|
192
|
+
false
|
193
|
+
end
|
194
|
+
|
195
|
+
def self.is_table_subtype
|
196
|
+
false
|
197
|
+
end
|
198
|
+
end
|