activefacts-rmap 1.7.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|