activefacts-rmap 1.7.1

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