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,433 @@
|
|
1
|
+
#
|
2
|
+
# ActiveFacts Relational mapping
|
3
|
+
# Reference from one ObjectType to another, used to decide the relational mapping.
|
4
|
+
#
|
5
|
+
# Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
|
6
|
+
#
|
7
|
+
# A Reference from one ObjectType to another is created for each many-1 or 1-1 relationship
|
8
|
+
# (including subtyping), and also for a unary role (implicitly to Boolean object_type).
|
9
|
+
# A 1-1 or subtyping reference should be created in only one direction, and may be flipped
|
10
|
+
# if needed.
|
11
|
+
#
|
12
|
+
# A reference to a object_type that's a table or is fully absorbed into a table will
|
13
|
+
# become a foreign key, otherwise it will absorb all that object_type's references.
|
14
|
+
#
|
15
|
+
# Reference objects update each object_type's list of the references *to* and *from* that object_type.
|
16
|
+
#
|
17
|
+
# Copyright (c) 2008 Clifford Heath. Read the LICENSE file.
|
18
|
+
#
|
19
|
+
|
20
|
+
module ActiveFacts
|
21
|
+
module RMap
|
22
|
+
|
23
|
+
# This class contains the core data structure used in composing a relational schema.
|
24
|
+
#
|
25
|
+
# A Reference is *from* one ObjectType *to* another ObjectType, and relates to the *from_role* and the *to_role*.
|
26
|
+
# When either ObjectType is an objectified fact type, the corresponding role is nil.
|
27
|
+
# When the Reference from_role is of a unary fact type, there's no to_role or to ObjectType.
|
28
|
+
# The final kind of Reference is a self-reference which is added to a ValueType that becomes a table.
|
29
|
+
#
|
30
|
+
# When the underlying fact type is a one-to-one (including an inheritance fact type), the Reference may be flipped.
|
31
|
+
#
|
32
|
+
# Each Reference has a name; an array of names in fact, in case of adjectives, etc.
|
33
|
+
# Each Refererence can produce the reading of the underlying fact type.
|
34
|
+
#
|
35
|
+
# A Reference is indexed in the player's *references_from* and *references_to*, and flipping updates those.
|
36
|
+
# Finally, a Reference may be marked as absorbing the whole referenced object, and that can flip too.
|
37
|
+
#
|
38
|
+
class Reference
|
39
|
+
attr_reader :from, :to # A "from" instance is related to one "to" instance
|
40
|
+
attr_reader :from_role, :to_role # For objectified facts, one role will be nil (a phantom)
|
41
|
+
attr_reader :fact_type
|
42
|
+
attr_accessor :fk_jump # True if this reference links a table to another in an FK (between absorbed references)
|
43
|
+
|
44
|
+
# A Reference is created from a object_type in regard to a role it plays
|
45
|
+
def initialize(from, role)
|
46
|
+
@fk_jump = false
|
47
|
+
@from = from
|
48
|
+
return unless role # All done if it's a self-value reference for a ValueType
|
49
|
+
@fact_type = role.fact_type
|
50
|
+
if @fact_type.all_role.size == 1
|
51
|
+
# @from_role is nil for a unary
|
52
|
+
@to_role = role
|
53
|
+
@to = role.fact_type.entity_type # nil unless the unary is objectified
|
54
|
+
elsif (role.fact_type.entity_type == @from) # role is in "from", an objectified fact type
|
55
|
+
@from_role = nil # Phantom role
|
56
|
+
@to_role = role
|
57
|
+
@to = @to_role.object_type
|
58
|
+
else
|
59
|
+
@from_role = role
|
60
|
+
@to = role.fact_type.entity_type # If set, to_role is a phantom
|
61
|
+
unless @to
|
62
|
+
raise "Illegal reference through >binary fact type" if @fact_type.all_role.size >2
|
63
|
+
@to_role = (role.fact_type.all_role-[role])[0]
|
64
|
+
@to = @to_role.object_type
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# What type of Role did this Reference arise from?
|
70
|
+
def role_type
|
71
|
+
role = @from_role||@to_role
|
72
|
+
role && role.role_type
|
73
|
+
end
|
74
|
+
|
75
|
+
# Is this Reference covered by a mandatory constraint (implicitly or explicitly)
|
76
|
+
def is_mandatory
|
77
|
+
!is_unary &&
|
78
|
+
(!@from_role || # All phantom roles of fact types are mandatory
|
79
|
+
@from_role.is_mandatory)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Is this Reference from a unary Role?
|
83
|
+
def is_unary
|
84
|
+
@to_role && @to_role.fact_type.all_role.size == 1
|
85
|
+
end
|
86
|
+
|
87
|
+
# If this Reference is to an objectified FactType, there is no *to_role*
|
88
|
+
def is_to_objectified_fact
|
89
|
+
# This case is the only one that cannot be used in the preferred identifier of @from
|
90
|
+
@to && !@to_role && @from_role
|
91
|
+
end
|
92
|
+
|
93
|
+
# If this Reference is from an objectified FactType, there is no *from_role*
|
94
|
+
def is_from_objectified_fact
|
95
|
+
@to && !@from_role && @to_role
|
96
|
+
end
|
97
|
+
|
98
|
+
# Is this reference an injected role as a result a ValueType being a table?
|
99
|
+
def is_self_value
|
100
|
+
!@to && !@to_role
|
101
|
+
end
|
102
|
+
|
103
|
+
# Is the *to* object_type fully absorbed through this reference?
|
104
|
+
def is_absorbing
|
105
|
+
@to && @to.absorbed_via == self
|
106
|
+
end
|
107
|
+
|
108
|
+
# Is this a simple reference?
|
109
|
+
def is_simple_reference
|
110
|
+
# It's a simple reference to a thing if that thing is a table,
|
111
|
+
# or is fully absorbed into another table but not via this reference.
|
112
|
+
@to && (@to.is_table or @to.absorbed_via && !is_absorbing)
|
113
|
+
end
|
114
|
+
|
115
|
+
# Return the array of names for the (perhaps implicit) *to_role* of this Reference
|
116
|
+
def to_names(is_prefix = true)
|
117
|
+
case
|
118
|
+
when is_unary
|
119
|
+
if @to && @to.fact_type && is_prefix
|
120
|
+
@to.name.camelwords
|
121
|
+
else
|
122
|
+
@to_role.fact_type.preferred_reading.text.gsub(/\{[0-9]\}/,'').strip.camelwords
|
123
|
+
end
|
124
|
+
when @to && !@to_role # @to is an objectified fact type so @to_role is a phantom
|
125
|
+
@to.name.camelwords
|
126
|
+
when !@to_role # Self-value role of an independent ValueType
|
127
|
+
@from.name.camelwords + ["Value"]
|
128
|
+
when @to_role.role_name # Named role
|
129
|
+
@to_role.role_name.camelwords
|
130
|
+
else # Use the name from the preferred reading
|
131
|
+
role_ref = @to_role.preferred_reference
|
132
|
+
[role_ref.leading_adjective, @to_role.object_type.name, role_ref.trailing_adjective].compact.map{|w| w.camelwords}.flatten.reject{|s| s == ''}
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Return the array of names for the (perhaps implicit) *from_role* of this Reference
|
137
|
+
def from_names
|
138
|
+
case
|
139
|
+
when @from && !@from_role # @from is an objectified fact type so @from_role is a phantom
|
140
|
+
@from.name.camelwords
|
141
|
+
when is_unary
|
142
|
+
if @from && @from.fact_type
|
143
|
+
@from.name.camelwords
|
144
|
+
else
|
145
|
+
@from_role.fact_type.preferred_reading.text.gsub(/\{[0-9]\}/,'').strip.camelwords
|
146
|
+
end
|
147
|
+
when !@from_role # Self-value role of an independent ValueType
|
148
|
+
@from.name.camelwords + ["Value"]
|
149
|
+
when @from_role.role_name # Named role
|
150
|
+
@from_role.role_name.camelwords
|
151
|
+
else # Use the name from the preferred reading
|
152
|
+
role_ref = @from_role.preferred_reference
|
153
|
+
[role_ref.leading_adjective, @from_role.object_type.name, role_ref.trailing_adjective].compact.map{|w| w.camelwords}.flatten.reject{|s| s == ''}
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def is_one_to_one
|
158
|
+
[:one_one, :subtype, :supertype].include?(role_type)
|
159
|
+
end
|
160
|
+
|
161
|
+
# For a one-to-one (or a subtyping fact type), reverse the direction.
|
162
|
+
def flip #:nodoc:
|
163
|
+
raise "Illegal flip of #{self}" unless @to and is_one_to_one
|
164
|
+
|
165
|
+
detabulate
|
166
|
+
mirror
|
167
|
+
tabulate
|
168
|
+
end
|
169
|
+
|
170
|
+
# Create a (non-tabulated) flipped version of this Reference. Careful not to tabulate it!
|
171
|
+
def mirror
|
172
|
+
if @to.absorbed_via == self
|
173
|
+
@to.absorbed_via = nil
|
174
|
+
@from.absorbed_via = self
|
175
|
+
end
|
176
|
+
|
177
|
+
# Flip the reference
|
178
|
+
@to, @from = @from, @to
|
179
|
+
@to_role, @from_role = @from_role, @to_role
|
180
|
+
self
|
181
|
+
end
|
182
|
+
|
183
|
+
def reversed
|
184
|
+
clone.mirror
|
185
|
+
end
|
186
|
+
|
187
|
+
def tabulate #:nodoc:
|
188
|
+
# Add to @to and @from's reference lists
|
189
|
+
@from.references_from << self
|
190
|
+
@to.references_to << self if @to # Guard against self-values
|
191
|
+
|
192
|
+
trace :references, "Adding #{to_s}"
|
193
|
+
self
|
194
|
+
end
|
195
|
+
|
196
|
+
def detabulate #:nodoc:
|
197
|
+
# Remove from @to and @from's reference lists if present
|
198
|
+
return unless @from.references_from.delete(self)
|
199
|
+
@to.references_to.delete self if @to # Guard against self-values
|
200
|
+
trace :references, "Dropping #{to_s}"
|
201
|
+
self
|
202
|
+
end
|
203
|
+
|
204
|
+
def to_s #:nodoc:
|
205
|
+
ref_type = fk_jump ? "jumping to" : (is_absorbing ? "absorbing" : "to")
|
206
|
+
"reference from #{@from.name}#{@to ? " #{ref_type} #{@to.name}" : ""}" + (@fact_type ? " in '#{@fact_type.default_reading}'" : "")
|
207
|
+
end
|
208
|
+
|
209
|
+
# The reading for the fact type underlying this Reference
|
210
|
+
def reading
|
211
|
+
is_self_value ? "#{from.name} has value" : @fact_type.reading_preferably_starting_with_role(@from_role).expand
|
212
|
+
end
|
213
|
+
|
214
|
+
def verbalised_path reverse = false
|
215
|
+
return "#{from.name} Value" if is_self_value
|
216
|
+
objectified = fact_type.entity_type
|
217
|
+
f = # Switch to the Link Fact Type if we're traversing an objectification
|
218
|
+
(to_role && to_role.link_fact_type) ||
|
219
|
+
(from_role && from_role.link_fact_type) ||
|
220
|
+
fact_type
|
221
|
+
|
222
|
+
start_role =
|
223
|
+
if objectified
|
224
|
+
target = reverse ? to : from
|
225
|
+
[to_role, from_role, f.all_role[0]].compact.detect{|role| role.object_type == target}
|
226
|
+
else
|
227
|
+
reverse ? to_role : from_role
|
228
|
+
end
|
229
|
+
reading = f.reading_preferably_starting_with_role(start_role)
|
230
|
+
(is_mandatory || is_unary ? '' : 'maybe ') +
|
231
|
+
reading.expand
|
232
|
+
end
|
233
|
+
|
234
|
+
def inspect #:nodoc:
|
235
|
+
to_s
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
module Metamodel #:nodoc:
|
241
|
+
class ObjectType
|
242
|
+
# Say whether the independence of this object is still under consideration
|
243
|
+
# This is used in detecting dependency cycles, such as occurs in the Metamodel
|
244
|
+
attr_accessor :tentative #:nodoc:
|
245
|
+
attr_writer :is_table # The two ObjectType subclasses provide the attr_reader method
|
246
|
+
|
247
|
+
def show_tabular #:nodoc:
|
248
|
+
(tentative ? "tentatively " : "") +
|
249
|
+
(is_table ? "" : "not ")+"a table"
|
250
|
+
end
|
251
|
+
|
252
|
+
def definitely_table #:nodoc:
|
253
|
+
@is_table = true
|
254
|
+
@tentative = false
|
255
|
+
end
|
256
|
+
|
257
|
+
def definitely_not_table #:nodoc:
|
258
|
+
@is_table = false
|
259
|
+
@tentative = false
|
260
|
+
end
|
261
|
+
|
262
|
+
def probably_table #:nodoc:
|
263
|
+
@is_table = true
|
264
|
+
@tentative = true
|
265
|
+
end
|
266
|
+
|
267
|
+
def probably_not_table #:nodoc:
|
268
|
+
@is_table = false
|
269
|
+
@tentative = true
|
270
|
+
end
|
271
|
+
|
272
|
+
# References from this ObjectType
|
273
|
+
def references_from
|
274
|
+
@references_from ||= []
|
275
|
+
end
|
276
|
+
|
277
|
+
# References to this ObjectType
|
278
|
+
def references_to
|
279
|
+
@references_to ||= []
|
280
|
+
end
|
281
|
+
|
282
|
+
# True if this ObjectType has any References (to or from)
|
283
|
+
def has_references #:nodoc:
|
284
|
+
@references_from || @references_to
|
285
|
+
end
|
286
|
+
|
287
|
+
def clear_references #:nodoc:
|
288
|
+
# Clear any previous references:
|
289
|
+
@references_to = nil
|
290
|
+
@references_from = nil
|
291
|
+
end
|
292
|
+
|
293
|
+
def populate_references #:nodoc:
|
294
|
+
all_role.each do |role|
|
295
|
+
# It's possible that this role is in an implicit or derived fact type. Skip it if so.
|
296
|
+
next if role.fact_type.is_a?(LinkFactType) or
|
297
|
+
# REVISIT: dafuq? Is this looking for a constraint over a derivation? This looks wrong.
|
298
|
+
role.fact_type.preferred_reading.role_sequence.all_role_ref.to_a[0].play or
|
299
|
+
# This is not yet actually set, and wouldn't handle constraint derivations anyhow:
|
300
|
+
role.variable_as_projection
|
301
|
+
|
302
|
+
populate_reference role
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
def populate_reference role #:nodoc:
|
307
|
+
role_type = role.role_type
|
308
|
+
trace :references, "#{name} has #{role_type} role in '#{role.fact_type.describe}'"
|
309
|
+
case role_type
|
310
|
+
when :many_one
|
311
|
+
ActiveFacts::RMap::Reference.new(self, role).tabulate # A simple reference
|
312
|
+
|
313
|
+
when :one_many
|
314
|
+
if role.fact_type.entity_type == self # A Role of this objectified FactType
|
315
|
+
ActiveFacts::RMap::Reference.new(self, role).tabulate # A simple reference; check that
|
316
|
+
else
|
317
|
+
# Can't absorb many of these into one of those
|
318
|
+
#trace :references, "Ignoring #{role_type} reference from #{name} to #{Reference.new(self, role).to.name}"
|
319
|
+
end
|
320
|
+
|
321
|
+
when :unary
|
322
|
+
ActiveFacts::RMap::Reference.new(self, role).tabulate # A simple reference
|
323
|
+
|
324
|
+
when :supertype # A subtype absorbs a reference to its supertype when separate, or all when partitioned
|
325
|
+
# REVISIT: Or when partitioned
|
326
|
+
raise "Internal error, expected TypeInheritance" unless role.fact_type.is_a?(ActiveFacts::Metamodel::TypeInheritance)
|
327
|
+
counterpart_role = (role.fact_type.all_role.to_a-[role])[0]
|
328
|
+
if role.fact_type.assimilation or counterpart_role.object_type.is_separate
|
329
|
+
trace :references, "supertype #{name} doesn't absorb a reference to separate subtype #{role.fact_type.subtype.name}"
|
330
|
+
else
|
331
|
+
r = ActiveFacts::RMap::Reference.new(self, role)
|
332
|
+
r.to.absorbed_via = r
|
333
|
+
trace :references, "supertype #{name} absorbs subtype #{r.to.name}"
|
334
|
+
r.tabulate
|
335
|
+
end
|
336
|
+
|
337
|
+
when :subtype # This object is a supertype, which can absorb the subtype unless that's independent
|
338
|
+
if role.fact_type.assimilation or is_separate
|
339
|
+
ActiveFacts::RMap::Reference.new(self, role).tabulate
|
340
|
+
# If partitioned, the supertype is absorbed into *each* subtype; a reference to the supertype needs to know which
|
341
|
+
else
|
342
|
+
# trace :references, "subtype #{name} is absorbed into #{role.fact_type.supertype.name}"
|
343
|
+
end
|
344
|
+
|
345
|
+
when :one_one
|
346
|
+
r = ActiveFacts::RMap::Reference.new(self, role)
|
347
|
+
|
348
|
+
# Decide which way the one-to-one is likely to go; it will be flipped later if necessary.
|
349
|
+
# Force the decision if just one is independent:
|
350
|
+
# REVISIT: Decide whether supertype assimilation can affect this
|
351
|
+
r.tabulate and return if is_separate and !r.to.is_separate
|
352
|
+
return if !is_separate and r.to.is_separate
|
353
|
+
|
354
|
+
if is_a?(ValueType)
|
355
|
+
# Never absorb an entity type into a value type
|
356
|
+
return if r.to.is_a?(EntityType) # Don't tabulate it
|
357
|
+
else
|
358
|
+
if r.to.is_a?(ValueType)
|
359
|
+
r.tabulate # Always absorb a value type into an entity type
|
360
|
+
return
|
361
|
+
end
|
362
|
+
|
363
|
+
# Force the decision if one EntityType identifies another:
|
364
|
+
if preferred_identifier.role_sequence.all_role_ref.detect{|rr| rr.role == r.to_role}
|
365
|
+
trace :references, "EntityType #{name} is identified by EntityType #{r.to.name}, so gets absorbed elsewhere"
|
366
|
+
return
|
367
|
+
end
|
368
|
+
if r.to.preferred_identifier.role_sequence.all_role_ref.detect{|rr| rr.role == role}
|
369
|
+
trace :references, "EntityType #{name} identifies EntityType #{r.to.name}, so absorbs it"
|
370
|
+
r.to.absorbed_via = r
|
371
|
+
# We can't be absorbed into our supertype!
|
372
|
+
# REVISIT: We might need to flip all one-to-ones as well
|
373
|
+
r.to.references_to.clone.map{|q|q.flip if q.to_role.role_type == :subtype }
|
374
|
+
r.tabulate
|
375
|
+
return
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
# Either both EntityTypes, or both ValueTypes.
|
380
|
+
# Make an arbitrary (but stable) decision which way to go. We might flip it later,
|
381
|
+
# but not frivolously; the Ruby API column name generation duplicates this logic.
|
382
|
+
unless r.from.name.downcase < r.to.name.downcase or
|
383
|
+
(r.from == r.to && references_to.detect{|ref| ref.to_role == role}) # one-to-one self reference, done already
|
384
|
+
r.tabulate
|
385
|
+
end
|
386
|
+
else
|
387
|
+
# REVISIT: Should we implicitly objectify this fact type here and add a spanning UC?
|
388
|
+
raise "Role #{role.object_type.name} in '#{role.fact_type.default_reading}' lacks a uniqueness constraint"
|
389
|
+
end
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
class EntityType < DomainObjectType
|
394
|
+
def populate_references #:nodoc:
|
395
|
+
if fact_type && fact_type.all_role.size > 1
|
396
|
+
# NOT: fact_type.all_role.each do |role| # Place roles in the preferred order instead:
|
397
|
+
fact_type.preferred_reading.role_sequence.all_role_ref.map(&:role).each do |role|
|
398
|
+
populate_reference role # Objectified fact role, handled specially
|
399
|
+
end
|
400
|
+
end
|
401
|
+
super
|
402
|
+
end
|
403
|
+
end
|
404
|
+
|
405
|
+
class Vocabulary
|
406
|
+
def populate_all_references #:nodoc:
|
407
|
+
trace :references, "Populating all object_type references" do
|
408
|
+
all_object_type.each do |object_type|
|
409
|
+
trace :references, "Populating references for #{object_type.name}" do
|
410
|
+
object_type.populate_references
|
411
|
+
end
|
412
|
+
end
|
413
|
+
end
|
414
|
+
show_all_references
|
415
|
+
end
|
416
|
+
|
417
|
+
def show_all_references
|
418
|
+
if trace :references
|
419
|
+
trace :references, "Finished object_type references" do
|
420
|
+
all_object_type.each do |object_type|
|
421
|
+
trace :references?, "#{object_type.name}:" do
|
422
|
+
object_type.references_from.each do |ref|
|
423
|
+
trace :references, "#{ref}"
|
424
|
+
end
|
425
|
+
end
|
426
|
+
end
|
427
|
+
end
|
428
|
+
end
|
429
|
+
end
|
430
|
+
end
|
431
|
+
|
432
|
+
end
|
433
|
+
end
|
@@ -0,0 +1,380 @@
|
|
1
|
+
#
|
2
|
+
# ActiveFacts Relational mapping
|
3
|
+
# Tables; Calculate the relational composition of a given Vocabulary.
|
4
|
+
# The composition consists of decisions about which ObjectTypes are tables,
|
5
|
+
# and what columns (absorbed roled) those tables will have.
|
6
|
+
#
|
7
|
+
# Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
|
8
|
+
#
|
9
|
+
# This module has the following known problems:
|
10
|
+
#
|
11
|
+
# * When a subtype has no mandatory roles, we should support an optional schema transformation step
|
12
|
+
# that introduces a boolean (is_subtype) to indicate it's that subtype.
|
13
|
+
#
|
14
|
+
|
15
|
+
require 'activefacts/rmap/reference'
|
16
|
+
|
17
|
+
module ActiveFacts
|
18
|
+
module Metamodel
|
19
|
+
|
20
|
+
class ValueType < DomainObjectType
|
21
|
+
def absorbed_via #:nodoc:
|
22
|
+
# ValueTypes aren't absorbed in the way EntityTypes are
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns true if this ValueType is a table
|
27
|
+
def is_table
|
28
|
+
return @is_table if @is_table != nil
|
29
|
+
|
30
|
+
# Always a table if marked so:
|
31
|
+
if is_separate
|
32
|
+
trace :absorption, "ValueType #{name} is declared independent or separate"
|
33
|
+
@tentative = false
|
34
|
+
return @is_table = true
|
35
|
+
end
|
36
|
+
|
37
|
+
# Only a table if it has references (to another ValueType)
|
38
|
+
if !references_from.empty? && !is_auto_assigned
|
39
|
+
trace :absorption, "#{name} is a table because it has #{references_from.size} references to it"
|
40
|
+
@is_table = true
|
41
|
+
else
|
42
|
+
@is_table = false
|
43
|
+
end
|
44
|
+
@tentative = false
|
45
|
+
|
46
|
+
@is_table
|
47
|
+
end
|
48
|
+
|
49
|
+
# Is this ValueType auto-assigned either at assert or on first save to the database?
|
50
|
+
def is_auto_assigned
|
51
|
+
type = self
|
52
|
+
while type
|
53
|
+
return true if type.name =~ /^Auto/ || type.transaction_phase
|
54
|
+
type = type.supertype
|
55
|
+
end
|
56
|
+
end
|
57
|
+
false
|
58
|
+
end
|
59
|
+
|
60
|
+
class EntityType < DomainObjectType
|
61
|
+
# A Reference from an entity type that fully absorbs this one
|
62
|
+
attr_accessor :absorbed_via #:nodoc:
|
63
|
+
attr_accessor :absorbed_mirror #:nodoc:
|
64
|
+
|
65
|
+
def is_auto_assigned #:nodoc:
|
66
|
+
false
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns true if this EntityType is a table
|
70
|
+
def is_table
|
71
|
+
return @is_table if @is_table != nil # We already make a guess or decision
|
72
|
+
|
73
|
+
@tentative = false
|
74
|
+
|
75
|
+
# Always a table if marked so
|
76
|
+
if is_separate
|
77
|
+
trace :absorption, "EntityType #{name} is declared independent or separate"
|
78
|
+
return @is_table = true
|
79
|
+
end
|
80
|
+
|
81
|
+
# Always a table if nowhere else to go, and has no one-to-ones that might flip:
|
82
|
+
if references_to.empty? and
|
83
|
+
!references_from.detect{|ref| ref.role_type == :one_one }
|
84
|
+
trace :absorption, "EntityType #{name} is presumed independent as it has nowhere to go"
|
85
|
+
return @is_table = true
|
86
|
+
end
|
87
|
+
|
88
|
+
# Subtypes may be partitioned or separate, in which case they're definitely tables.
|
89
|
+
# Otherwise, if their identification is inherited from a supertype, they're definitely absorbed.
|
90
|
+
# If theey have separate identification, it might absorb them.
|
91
|
+
if (!supertypes.empty?)
|
92
|
+
as_ti = all_supertype_inheritance.detect{|ti| ti.assimilation}
|
93
|
+
@is_table = as_ti != nil
|
94
|
+
if @is_table
|
95
|
+
trace :absorption, "EntityType #{name} is #{as_ti.assimilation} from supertype #{as_ti.supertype}"
|
96
|
+
else
|
97
|
+
identifying_fact_type = preferred_identifier.role_sequence.all_role_ref.to_a[0].role.fact_type
|
98
|
+
if identifying_fact_type.is_a?(TypeInheritance)
|
99
|
+
trace :absorption, "EntityType #{name} is absorbed into supertype #{supertypes[0].name}"
|
100
|
+
@is_table = false
|
101
|
+
else
|
102
|
+
# Possibly absorbed, we'll have to see how that pans out
|
103
|
+
@tentative = true
|
104
|
+
end
|
105
|
+
end
|
106
|
+
return @is_table
|
107
|
+
end
|
108
|
+
|
109
|
+
# If the preferred_identifier includes an auto_assigned ValueType
|
110
|
+
# and this object is absorbed in more than one place, we need a table
|
111
|
+
# to manage the auto-assignment.
|
112
|
+
if references_to.size > 1 and
|
113
|
+
preferred_identifier.role_sequence.all_role_ref.detect {|rr|
|
114
|
+
next false unless rr.role.object_type.is_a? ValueType
|
115
|
+
rr.role.object_type.is_auto_assigned
|
116
|
+
}
|
117
|
+
trace :absorption, "#{name} has an auto-assigned counter in its ID, so must be a table"
|
118
|
+
@tentative = false
|
119
|
+
return @is_table = true
|
120
|
+
end
|
121
|
+
|
122
|
+
@tentative = true
|
123
|
+
@is_table = true
|
124
|
+
end
|
125
|
+
end # EntityType class
|
126
|
+
|
127
|
+
class Role #:nodoc:
|
128
|
+
def role_type
|
129
|
+
# TypeInheritance roles are always 1:1
|
130
|
+
if TypeInheritance === fact_type
|
131
|
+
return object_type == fact_type.supertype ? :supertype : :subtype
|
132
|
+
end
|
133
|
+
|
134
|
+
# Always N:1 if unary:
|
135
|
+
return :unary if fact_type.all_role.size == 1
|
136
|
+
|
137
|
+
# List the UCs on this fact type:
|
138
|
+
all_uniqueness_constraints =
|
139
|
+
fact_type.all_role.map do |fact_role|
|
140
|
+
fact_role.all_role_ref.map do |rr|
|
141
|
+
rr.role_sequence.all_presence_constraint.select do |pc|
|
142
|
+
pc.max_frequency == 1
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end.flatten.uniq
|
146
|
+
|
147
|
+
to_1 =
|
148
|
+
all_uniqueness_constraints.
|
149
|
+
detect do |c|
|
150
|
+
(rr = c.role_sequence.all_role_ref.single) and
|
151
|
+
rr.role == self
|
152
|
+
end
|
153
|
+
# REVISIT: check mapping pragmas, e.g. by to_1.concept.all_concept_annotation.detect{|ca| ca.mapping_annotation == 'separate'}
|
154
|
+
|
155
|
+
if fact_type.entity_type
|
156
|
+
# This is a role in an objectified fact type
|
157
|
+
from_1 = true
|
158
|
+
else
|
159
|
+
# It's to-1 if a UC exists over roles of this FT that doesn't cover this role:
|
160
|
+
from_1 = all_uniqueness_constraints.detect{|uc|
|
161
|
+
!uc.role_sequence.all_role_ref.detect{|rr| rr.role == self || rr.role.fact_type != fact_type}
|
162
|
+
}
|
163
|
+
end
|
164
|
+
|
165
|
+
if from_1
|
166
|
+
return to_1 ? :one_one : :one_many
|
167
|
+
else
|
168
|
+
return to_1 ? :many_one : :many_many
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
end
|
173
|
+
|
174
|
+
class Vocabulary
|
175
|
+
@@relational_transforms = []
|
176
|
+
|
177
|
+
# return an Array of ObjectTypes that will have their own tables
|
178
|
+
def tables
|
179
|
+
decide_tables if !@tables
|
180
|
+
@@relational_transforms.each{|tr| tr.call(self)}
|
181
|
+
@tables
|
182
|
+
end
|
183
|
+
|
184
|
+
def self.relational_transform &block
|
185
|
+
# Add this block to the additional transformations which will be applied
|
186
|
+
# to the relational schema after the initial absorption.
|
187
|
+
# For example, to perform injection of surrogate keys to replace composite keys...
|
188
|
+
@@relational_transforms << block
|
189
|
+
end
|
190
|
+
|
191
|
+
def wipe_existing_mapping
|
192
|
+
all_object_type.each do |object_type|
|
193
|
+
object_type.clear_references
|
194
|
+
object_type.wipe_columns
|
195
|
+
object_type.is_table = nil # Undecided; force an attempt to decide
|
196
|
+
object_type.tentative = true # Uncertain
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def decide_tables #:nodoc:
|
201
|
+
# Strategy:
|
202
|
+
# 1) Populate references for all ObjectTypes
|
203
|
+
# 2) Decide which ObjectTypes must be and must not be tables
|
204
|
+
# a. ObjectTypes labelled is_independent/separate are tables (See the is_table methods above)
|
205
|
+
# b. Entity types having no references to them must be tables
|
206
|
+
# c. subtypes are not tables unless marked with assimilation = separate or partitioned
|
207
|
+
# d. ValueTypes are never tables unless they independent or can have references (to other ValueTypes)
|
208
|
+
# e. An EntityType having an identifying AutoInc field must be a table unless it has exactly one reference
|
209
|
+
# f. An EntityType whose only reference is through its single preferred_identifier role gets absorbed
|
210
|
+
# g. An EntityType that must has references other than its PI must be a table (unless it has exactly one reference to it)
|
211
|
+
# h. supertypes are elided if all roles are absorbed into subtypes:
|
212
|
+
# - partitioned subtype exhaustion
|
213
|
+
# - subtype extension where supertype has only PI roles and no AutoInc
|
214
|
+
# 3) any ValueType that has references from it must become a table if not already
|
215
|
+
|
216
|
+
wipe_existing_mapping
|
217
|
+
|
218
|
+
populate_all_references
|
219
|
+
|
220
|
+
trace :absorption, "Calculating relational composition" do
|
221
|
+
# Evaluate the possible independence of each object_type, building an array of object_types of indeterminate status:
|
222
|
+
undecided =
|
223
|
+
all_object_type.select do |object_type|
|
224
|
+
object_type.is_table # Ask it whether it thinks it should be a table
|
225
|
+
object_type.tentative # Selection criterion
|
226
|
+
end
|
227
|
+
|
228
|
+
if trace :absorption, "Generating tables, #{undecided.size} undecided, already decided ones are"
|
229
|
+
(all_object_type-undecided).each {|object_type|
|
230
|
+
next if ValueType === object_type && !object_type.is_table # Skip unremarkable cases
|
231
|
+
trace :absorption do
|
232
|
+
trace :absorption, "#{object_type.name} is #{object_type.is_table ? "" : "not "}a table#{object_type.tentative ? ", tentatively" : ""}"
|
233
|
+
end
|
234
|
+
}
|
235
|
+
end
|
236
|
+
|
237
|
+
pass = 0
|
238
|
+
begin # Loop while we continue to make progress
|
239
|
+
pass += 1
|
240
|
+
trace :absorption, "Starting composition pass #{pass} with #{undecided.size} undecided tables"
|
241
|
+
possible_flips = {} # A hash by table containing an array of references that can be flipped
|
242
|
+
finalised = # Make an array of things we finalised during this pass
|
243
|
+
undecided.select do |object_type|
|
244
|
+
trace :absorption, "Considering #{object_type.name}:" do
|
245
|
+
trace :absorption, "refs to #{object_type.name} are from #{object_type.references_to.map{|ref| ref.from.name}*", "}" if object_type.references_to.size > 0
|
246
|
+
trace :absorption, "refs from #{object_type.name} are to #{object_type.references_from.map{|ref| ref.to ? ref.to.name : ref.fact_type.default_reading}*", "}" if object_type.references_from.size > 0
|
247
|
+
|
248
|
+
# Always absorb an objectified unary into its role player:
|
249
|
+
if object_type.fact_type && object_type.fact_type.all_role.size == 1
|
250
|
+
trace :absorption, "Absorb objectified unary #{object_type.name} into #{object_type.fact_type.entity_type.name}"
|
251
|
+
object_type.definitely_not_table
|
252
|
+
next object_type
|
253
|
+
end
|
254
|
+
|
255
|
+
# If the PI contains one role only, played by an entity type that can absorb us, do that.
|
256
|
+
pi_roles = object_type.preferred_identifier.role_sequence.all_role_ref.map(&:role)
|
257
|
+
trace :absorption, "pi_roles are played by #{pi_roles.map{|role| role.object_type.name}*", "}"
|
258
|
+
first_pi_role = pi_roles[0]
|
259
|
+
pi_ref = nil
|
260
|
+
if pi_roles.size == 1 and
|
261
|
+
object_type.references_to.detect do |ref|
|
262
|
+
if ref.from_role == first_pi_role and ref.from.is_a?(EntityType) # and ref.is_mandatory # REVISIT
|
263
|
+
pi_ref = ref
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
trace :absorption, "#{object_type.name} is fully absorbed along its sole reference path into entity type #{pi_ref.from.name}"
|
268
|
+
object_type.definitely_not_table
|
269
|
+
next object_type
|
270
|
+
end
|
271
|
+
|
272
|
+
# If there's more than one absorption path and any functional dependencies that can't absorb us, it's a table
|
273
|
+
non_identifying_refs_from =
|
274
|
+
object_type.references_from.reject{|ref|
|
275
|
+
pi_roles.include?(ref.to_role)
|
276
|
+
}
|
277
|
+
trace :absorption, "#{object_type.name} has #{non_identifying_refs_from.size} non-identifying functional roles"
|
278
|
+
|
279
|
+
=begin
|
280
|
+
# This is kinda arbitrary. We need a policy for evaluating optional flips, so we can decide if they "improve" things.
|
281
|
+
# The flipping that occurs below always eliminates a table by absorption, but this doesn't.
|
282
|
+
|
283
|
+
# If all non-identifying functional roles are one-to-ones that can be flipped, do that:
|
284
|
+
if non_identifying_refs_from.all? { |ref| ref.role_type == :one_one && (ref.to.is_table || ref.to.tentative) }
|
285
|
+
trace :absorption, "Flipping references from #{object_type.name}" do
|
286
|
+
non_identifying_refs_from.each do |ref|
|
287
|
+
trace :absorption, "Flipping #{ref}"
|
288
|
+
ref.flip
|
289
|
+
end
|
290
|
+
end
|
291
|
+
non_identifying_refs_from = []
|
292
|
+
end
|
293
|
+
=end
|
294
|
+
|
295
|
+
if object_type.references_to.size > 1 and
|
296
|
+
non_identifying_refs_from.size > 0
|
297
|
+
trace :absorption, "#{object_type.name} has non-identifying functional dependencies so 3NF requires it be a table"
|
298
|
+
object_type.definitely_table
|
299
|
+
next object_type
|
300
|
+
end
|
301
|
+
|
302
|
+
absorption_paths =
|
303
|
+
(
|
304
|
+
non_identifying_refs_from.reject do |ref|
|
305
|
+
!ref.to or ref.to.absorbed_via == ref
|
306
|
+
end+object_type.references_to
|
307
|
+
).reject do |ref|
|
308
|
+
next true if !ref.to.is_table or !ref.is_one_to_one
|
309
|
+
|
310
|
+
# Don't absorb an object along a non-mandatory role (otherwise if it doesn't play that role, it can't exist either)
|
311
|
+
from_is_mandatory = !!ref.is_mandatory
|
312
|
+
to_is_mandatory = !ref.to_role || !!ref.to_role.is_mandatory
|
313
|
+
|
314
|
+
bad = !(ref.from == object_type ? from_is_mandatory : to_is_mandatory)
|
315
|
+
trace :absorption, "Not absorbing #{object_type.name} through non-mandatory #{ref}" if bad
|
316
|
+
bad
|
317
|
+
end
|
318
|
+
|
319
|
+
# If this object can be fully absorbed, do that (might require flipping some references)
|
320
|
+
if absorption_paths.size > 0
|
321
|
+
trace :absorption, "#{object_type.name} is fully absorbed through #{absorption_paths.inspect}"
|
322
|
+
absorption_paths.each do |ref|
|
323
|
+
trace :absorption, "Flipping #{ref} so #{object_type.name} can be absorbed"
|
324
|
+
ref.flip if object_type == ref.from
|
325
|
+
end
|
326
|
+
object_type.definitely_not_table
|
327
|
+
next object_type
|
328
|
+
end
|
329
|
+
|
330
|
+
if non_identifying_refs_from.size == 0
|
331
|
+
# REVISIT: This allows absorption along a non-mandatory role of a objectified fact type
|
332
|
+
# and object_type.references_to.all?{|ref| ref.is_mandatory }
|
333
|
+
# and (!object_type.is_a?(EntityType) ||
|
334
|
+
# # REVISIT: The roles may be collectively but not individually mandatory.
|
335
|
+
# object_type.references_to.detect { |ref| !ref.from_role || ref.from_role.is_mandatory })
|
336
|
+
trace :absorption, "#{object_type.name} is fully absorbed in #{object_type.references_to.size} places: #{object_type.references_to.map{|ref| ref.from.name}*", "}"
|
337
|
+
object_type.definitely_not_table
|
338
|
+
next object_type
|
339
|
+
end
|
340
|
+
|
341
|
+
false # Failed to decide about this entity_type this time around
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
undecided -= finalised
|
346
|
+
trace :absorption, "Finalised #{finalised.size} this pass: #{finalised.map{|f| f.name}*", "}"
|
347
|
+
end while !finalised.empty?
|
348
|
+
|
349
|
+
# A ValueType that isn't explicitly a table and isn't needed anywhere doesn't matter,
|
350
|
+
# unless it should absorb something else (another ValueType is all it could be):
|
351
|
+
all_object_type.each do |object_type|
|
352
|
+
if (!object_type.is_table and object_type.references_to.size == 0 and object_type.references_from.size > 0)
|
353
|
+
if !object_type.references_from.detect{|r| !r.is_one_to_one || !r.to.is_table}
|
354
|
+
trace :absorption, "Flipping references from #{object_type.name}; they're all to tables"
|
355
|
+
object_type.references_from.map(&:flip)
|
356
|
+
else
|
357
|
+
trace :absorption, "Making #{object_type.name} a table; it has nowhere else to go and needs to absorb things"
|
358
|
+
object_type.probably_table
|
359
|
+
end
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
# Now, evaluate all possibilities of the tentative assignments
|
364
|
+
# Incomplete. Apparently unnecessary as well... so far. We'll see.
|
365
|
+
if trace :absorption
|
366
|
+
undecided.each do |object_type|
|
367
|
+
trace :absorption, "Unable to decide independence of #{object_type.name}, going with #{object_type.show_tabular}"
|
368
|
+
end
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
@tables =
|
373
|
+
all_object_type.
|
374
|
+
select { |f| f.is_table }.
|
375
|
+
sort_by { |table| table.name }
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
end
|
380
|
+
end
|