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.
@@ -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