activefacts-compositions 1.9.10 → 1.9.12

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,798 @@
1
+ #
2
+ # ActiveFacts Compositions, DocGraph Compositor.
3
+ #
4
+ # Computes an Document/Semantic Graph schema.
5
+ #
6
+ # Copyright (c) 2017 Factil Pty Ltd. Read the LICENSE file.
7
+ #
8
+ require "activefacts/compositions"
9
+
10
+ module ActiveFacts
11
+ module Metamodel
12
+ class Composite
13
+ def is_document
14
+ @isa_document = true
15
+ end
16
+
17
+ def is_document?
18
+ @isa_document
19
+ end
20
+
21
+ def is_triple
22
+ @isa_triple = true
23
+ end
24
+
25
+ def is_triple?
26
+ @isa_triple
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ module ActiveFacts
33
+ module Compositions
34
+ class DocGraph < Compositor
35
+ MM = ActiveFacts::Metamodel unless const_defined?(:MM)
36
+ TRIPLE_ANNOTATION = /triple/
37
+
38
+ def self.options
39
+ {
40
+ nested: ['Boolean', "Compose nested documents"],
41
+ }.merge(Compositor.options)
42
+ end
43
+
44
+ def initialize constellation, name, options = {}
45
+ # Extract recognised options:
46
+ @option_nested = options.delete('nested')
47
+ super constellation, name, options, 'DocGraph'
48
+ end
49
+
50
+ def generate
51
+ super
52
+
53
+ trace :docgraph_details!, "Generating docgraph composition" do
54
+ # Make a data structure to help in computing the documents
55
+ make_candidates
56
+
57
+ # Apply any obvious document/graph factors
58
+ assign_default_docgraph
59
+
60
+ # Figure out how best to absorb things to reduce the number of documents
61
+ optimise_absorption
62
+
63
+ # Actually make a Composite object for each document and triple:
64
+ make_composites
65
+
66
+ # If a value type has been mapped to a document, add a property to hold its value
67
+ inject_value_fields
68
+
69
+ # # Inject surrogate keys if the options ask for that
70
+ # inject_surrogates if @option_surrogates
71
+
72
+ # # Remove the un-used absorption paths
73
+ # delete_reverse_absorptions
74
+
75
+ # Traverse the absorbed objects to build the path to each required property, including foreign keys:
76
+ absorb_all_properties
77
+
78
+ # Remove mappings for objects we have absorbed
79
+ clean_unused_mappings
80
+ end
81
+
82
+ trace :docgraph!, "Full #{self.class.basename} composition" do
83
+ @document_composites.values.sort_by{|composite| composite.mapping.name}.each do |composite|
84
+ composite.show_trace
85
+ end
86
+ @triple_composites.values.sort_by{|composite| composite.mapping.name}.each do |composite|
87
+ composite.show_trace
88
+ end
89
+ end
90
+ end
91
+
92
+ def make_candidates
93
+ @candidates = @binary_mappings.inject({}) do |hash, (absorption, mapping)|
94
+ hash[mapping.object_type] = Candidate.new(self, mapping)
95
+ hash
96
+ end
97
+ end
98
+
99
+ def assign_default_docgraph
100
+ trace :docgraph_defaults!, "Preparing DocGraph composition by setting default assumptions" do
101
+ @candidates.each do |object_type, candidate|
102
+ candidate.assign_default(@composition)
103
+ end
104
+ end
105
+ end
106
+
107
+ def optimise_absorption
108
+ trace :docgraph_optimiser!, "Optimise DocGraph Composition" do
109
+ undecided = @candidates.keys.select{|object_type| @candidates[object_type].is_tentative}
110
+ pass = 0
111
+ finalised = []
112
+ begin
113
+ pass += 1
114
+ trace :docgraph_optimiser, "Starting optimisation pass #{pass}" do
115
+ finalised = optimise_absorption_pass(undecided)
116
+ end
117
+ trace :docgraph_optimiser, "Finalised #{finalised.size} on this pass: #{finalised.map{|f| f.name}*', '}"
118
+ undecided -= finalised
119
+ end while !finalised.empty?
120
+ end
121
+ end
122
+
123
+ def optimise_absorption_pass undecided
124
+ undecided.select do |object_type|
125
+ candidate = @candidates[object_type]
126
+ trace :docgraph_optimiser, "Considering possible status of #{object_type.name}" do
127
+
128
+ # Rule 1: Always absorb an objectified unary into its role player (unless its forced to be separate)
129
+ if !object_type.is_separate && (f = object_type.fact_type) && f.all_role.size == 1
130
+ absorbing_ref = candidate.mapping.all_member.detect{|a| a.is_a?(MM::Absorption) and a.child_role.base_role == f.all_role.single}
131
+ raise "REVISIT: Internal error" unless absorbing_ref.parent_role.object_type == object_type
132
+ absorbing_ref = absorbing_ref.flip!
133
+ candidate.full_absorption =
134
+ @constellation.FullAbsorption(composition: @composition, absorption: absorbing_ref, object_type: object_type)
135
+ trace :docgraph_optimiser, "Fully absorb objectified unary #{object_type.name} into #{f.all_role.single.object_type.name}"
136
+ candidate.definitely_not_document
137
+ next object_type
138
+ end
139
+
140
+ # # Rule 2: If the preferred_identifier contains one role only, played by an entity type that can absorb us, do that:
141
+ # # (Leave pi_roles intact for further use below)
142
+ # absorbing_ref = nil
143
+ pi_roles = []
144
+ # if object_type.is_a?(MM::EntityType) and # We're an entity type
145
+ # pi_roles = object_type.preferred_identifier_roles and # Our PI
146
+ # pi_roles.size == 1 and # has one role
147
+ # single_pi_role = pi_roles[0] and # that role is
148
+ # single_pi_role.object_type.is_a?(MM::EntityType) and # played by another Entity Type
149
+ # absorbing_ref =
150
+ # candidate.mapping.all_member.detect do |absorption|
151
+ # absorption.is_a?(MM::Absorption) && absorption.child_role.base_role == single_pi_role
152
+ # end
153
+ #
154
+ # absorbing_ref = absorbing_ref.forward_absorption || absorbing_ref.flip!
155
+ # candidate.full_absorption =
156
+ # @constellation.FullAbsorption(composition: @composition, absorption: absorbing_ref, object_type: object_type)
157
+ # trace :docgraph_optimiser, "EntityType #{single_pi_role.object_type.name} identifies EntityType #{object_type.name}, so fully absorb it via #{absorbing_ref.inspect}"
158
+ # candidate.definitely_not_document
159
+ # next object_type
160
+ # end
161
+
162
+ # Rule 3: If there's more than one absorption path and any functional dependencies that can't absorb us, it's a document
163
+ trace :docgraph_optimiser, "From-references for #{object_type.name}(#{pi_roles.map(&:object_type).map(&:name)*', '}) are #{candidate.references_from.map(&:inspect)*', '}"
164
+ non_identifying_refs_from =
165
+ candidate.references_from.reject do |member|
166
+ case member
167
+ when MM::Absorption
168
+ pi_roles.include?(member.child_role.base_role)
169
+ when MM::Indicator
170
+ pi_roles.include?(member.role)
171
+ else
172
+ false
173
+ end
174
+ end
175
+ trace :docgraph_optimiser, "#{object_type.name} has #{non_identifying_refs_from.size} non-identifying functional roles" do
176
+ non_identifying_refs_from.each do |a|
177
+ trace :docgraph_optimiser, a.inspect
178
+ end
179
+ end
180
+
181
+ trace :docgraph_optimiser, "#{object_type.name} has #{candidate.references_to.size} references to it" do
182
+ candidate.references_to.each do |a|
183
+ trace :docgraph_optimiser, a.inspect
184
+ end
185
+ end
186
+
187
+ # Both of these conditions are not relevant for documents
188
+ # if candidate.references_to.size > 1 and # More than one place wants us
189
+ # non_identifying_refs_from.size > 0 # And we carry dependent values so cannot be absorbed
190
+ # trace :docgraph_optimiser, "#{object_type.name} has #{non_identifying_refs_from.size} non-identifying functional dependencies and #{candidate.references_to.size} absorption paths so it is a document"
191
+ # candidate.definitely_document
192
+ # next object_type
193
+ # end
194
+
195
+ # At this point, this object either has no functional dependencies or only one place it would be absorbed
196
+ next false if !candidate.is_document # We can't reduce the number of tables by absorbing this one
197
+
198
+ absorption_paths =
199
+ ( non_identifying_refs_from + # But we should exclude any that are already involved in an absorption; pre-decided ET=>ET or supertype absorption!
200
+ candidate.references_to # These are our reverse absorptions that could absorb us
201
+ ).select do |a|
202
+ next false unless a.is_a?(MM::Absorption) # Skip Indicators, we can't be absorbed there
203
+ child_candidate = @candidates[a.child_role.object_type]
204
+
205
+ # It's ok if we absorbed them already
206
+ next true if a.full_absorption && child_candidate.full_absorption.absorption != a
207
+
208
+ # If our counterpart is a full absorption, don't try to reverse that!
209
+ next false if (aa = (a.forward_absorption || a.reverse_absorption)) && aa.full_absorption
210
+
211
+ # Otherwise the other end must already be a document or fully absorbed into one
212
+ next false unless child_candidate.nil? || child_candidate.is_document || child_candidate.full_absorption
213
+
214
+ next false unless a.child_role.is_unique && a.parent_role.is_unique # Must be one-to-one
215
+
216
+ # next true if pi_roles.size == 1 && pi_roles.include?(a.parent_role) # Allow the sole identifying role for this object
217
+ next false unless a.parent_role.is_mandatory # Don't absorb an object along a non-mandatory role
218
+ true
219
+ end
220
+
221
+ trace :docgraph_optimiser, "#{object_type.name} has #{absorption_paths.size} absorption paths"
222
+
223
+ # # Rule 4: If this object can be fully absorbed along non-identifying roles, do that (maybe flip some absorptions)
224
+ # if absorption_paths.size > 0
225
+ # trace :docgraph_optimiser, "#{object_type.name} is fully absorbed in #{absorption_paths.size} places" do
226
+ # absorption_paths.each do |a|
227
+ # a = a.flip! if a.forward_absorption
228
+ # trace :docgraph_optimiser, "#{object_type.name} is fully absorbed via #{a.inspect}"
229
+ # end
230
+ # end
231
+ #
232
+ # candidate.definitely_not_document
233
+ # next object_type
234
+ # end
235
+
236
+ # Rule 5: If this object has no functional dependencies (only its identifier), it can be absorbed in multiple places
237
+ # We don't create FullAbsorptions, because they're only used to resolve references to this object; and there are none here
238
+ refs_to = candidate.references_to.reject{|a|a.parent_role.base_role.is_identifying}
239
+ if !refs_to.empty? and non_identifying_refs_from.size == 0
240
+ refs_to.map! do |a|
241
+ a = a.flip! if a.reverse_absorption # We were forward, but the other end must be
242
+ a.forward_absorption
243
+ end
244
+ trace :docgraph_optimiser, "#{object_type.name} is fully absorbed in #{refs_to.size} places: #{refs_to.map{|ref| ref.inspect}*", "}"
245
+ candidate.definitely_not_document
246
+ next object_type
247
+ end
248
+
249
+ false # Otherwise we failed to make a decision about this object type
250
+ end
251
+ end
252
+ end
253
+
254
+ def top component
255
+ component.parent ? top(component.parent) : component
256
+ end
257
+
258
+ # Remove the unused reverse absorptions:
259
+ def delete_reverse_absorptions
260
+ # @binary_mappings.each do |object_type, mapping|
261
+ # mapping.all_member.to_a. # Avoid problems with deletion from all_member
262
+ # each do |member|
263
+ # next unless member.is_a?(MM::Absorption)
264
+ # next unless member.forward_absorption
265
+ # # retract if this absorption is not in a document or the forward absorption is in a document
266
+ # member.retract if @document_composites[top(member.forward_absorption).object_type] || !@document_composites[top(member).object_type]
267
+ # end
268
+ # mapping.re_rank
269
+ # end
270
+ end
271
+
272
+ # After all document/triple decisions are made, convert Mappings for triples into Composites and retract the rest:
273
+ def make_composites
274
+ @document_composites = {}
275
+ @triple_composites = {}
276
+ @candidates.keys.to_a.each do |object_type|
277
+ candidate = @candidates[object_type]
278
+
279
+ if (candidate.is_document or candidate.is_triple) and !candidate.is_tentative
280
+ make_composite(candidate)
281
+ else
282
+ @candidates.delete(object_type)
283
+ end
284
+ end
285
+ end
286
+
287
+ def make_composite candidate
288
+ mapping = candidate.mapping
289
+ composite = @constellation.Composite(mapping, composition: @composition)
290
+ if candidate.is_document
291
+ @document_composites[mapping.object_type] = composite
292
+ composite.is_document
293
+ else
294
+ @triple_composites[mapping.object_type] = composite
295
+ composite.is_triple
296
+ end
297
+ end
298
+
299
+ # Inject a ValueField for each value type that is a document:
300
+ def inject_value_fields
301
+ @document_composites.each do |key, composite|
302
+ mapping = composite.mapping
303
+ if mapping.object_type.is_a?(MM::ValueType) and # Composite needs a ValueField
304
+ !mapping.all_member.detect{|m| m.is_a?(MM::ValueField)} # And don't already have one
305
+ trace :docgraph_properties, "Adding value field for #{mapping.object_type.name}"
306
+ @constellation.ValueField(
307
+ :new,
308
+ parent: mapping,
309
+ name: mapping.object_type.name+" Value",
310
+ object_type: mapping.object_type
311
+ )
312
+ mapping.re_rank
313
+ end
314
+ end
315
+ end
316
+
317
+ def clean_unused_mappings
318
+ @candidates.keys.to_a.each do |object_type|
319
+ candidate = @candidates[object_type]
320
+ next if candidate.is_document or candidate.is_triple
321
+ mapping = candidate.mapping
322
+ mapping.retract
323
+ @binary_mappings.delete(object_type)
324
+ end
325
+ end
326
+
327
+ def is_empty_inheritance mapping
328
+ # Cannot be an empty inheritance unless it's an TypeInheritance absorption
329
+ return false if !mapping.is_a?(MM::Absorption) || !mapping.parent_role.fact_type.is_a?(MM::TypeInheritance)
330
+
331
+ # It's empty if it's a TypeInheritance which has no non-empty members
332
+ !mapping.all_member.to_a.any? do |member|
333
+ !is_empty_inheritance(member)
334
+ end
335
+ end
336
+
337
+ def elide_empty_inheritance mapping
338
+ mapping.all_member.to_a.each do |member|
339
+ if member.is_a?(MM::Absorption) && member.parent_role.fact_type.is_a?(MM::TypeInheritance)
340
+ elide_empty_inheritance(member)
341
+ if member.all_member.size == 0
342
+ trace :docgraph, "Retracting empty inheritance #{member.inspect}"
343
+ member.retract
344
+ end
345
+ end
346
+ end
347
+ end
348
+
349
+ # Absorb all items which aren't documents (and keys to those which are) recursively
350
+ def absorb_all_properties
351
+ trace :docgraph_properties!, "Computing contents of all documents and triples" do
352
+ @document_composites.values.sort_by{|composite| composite.mapping.name}.each do |composite|
353
+ trace :docgraph_properties, "Computing contents of #{composite.mapping.name}" do
354
+ absorb_all(composite.mapping, composite.mapping)
355
+ end
356
+ end
357
+ end
358
+ end
359
+
360
+ #
361
+ # Rename parents functions defined because they are used in both Staging and Datavault subclasses
362
+ #
363
+ def apply_name pattern, name
364
+ pattern.sub(/\+/, name)
365
+ end
366
+
367
+ def rename_parents
368
+ @document_composites.each do |key, composite|
369
+ composite.mapping.name = apply_name(@option_stg_name, composite.mapping.name)
370
+ end
371
+ end
372
+
373
+ # This member is an Absorption. Process it recursively, either absorbing all its members if it is not a document,
374
+ # deleting it if it is a semantic triple or just keeping the key if it is not a semantic triple
375
+ def absorb_subdoc mapping, member, paths, stack
376
+ trace :docgraph_properties, "Absorb subdoc of #{member.inspect} into #{mapping.name}" do
377
+ # In the DocGraph composition, either absorb the contents or devolve a triple
378
+ trace :docgraph_properties, "(parent #{member.parent_role.object_type.name}, child #{member.child_role.object_type.name})"
379
+ child_object_type = member.child_role.object_type
380
+ child_mapping = @binary_mappings[child_object_type]
381
+ if @triple_composites[child_object_type]
382
+ trace :docgraph_triple, "Eliminate #{child_object_type.name} subdoc"
383
+ member.retract
384
+ mapping.re_rank
385
+ return
386
+ end
387
+
388
+ # Is our target object_type fully absorbed (and not through this absorption)?
389
+ full_absorption = child_object_type.all_full_absorption[@composition]
390
+ # We can't use member.full_absorption here, as it's not populated on forked copies
391
+ # if full_absorption && full_absorption != member.full_absorption
392
+ if full_absorption && full_absorption.absorption.parent_role.fact_type != member.parent_role.fact_type
393
+
394
+ # REVISIT: This should be done by recursing to absorb_key, not using a loop
395
+ absorption = member # Retain this for the ForeignKey
396
+ begin # Follow transitive target absorption
397
+ member = mirror(full_absorption.absorption, member)
398
+ child_object_type = full_absorption.absorption.parent_role.object_type
399
+ end while full_absorption = child_object_type.all_full_absorption[@composition]
400
+ child_mapping = @binary_mappings[child_object_type]
401
+
402
+ trace :docgraph_properties, "Absorbing all of #{member.child_role.name} in #{member.inspect_reading}"
403
+ absorb_all(member, child_mapping, paths, stack)
404
+ return
405
+ end
406
+
407
+ absorb_all(member, child_mapping, paths, stack)
408
+ end
409
+ end
410
+
411
+ # Handle the reverse absorptions of the mapping
412
+ def absorb_nested mapping, member, paths, stack
413
+ trace :docgraph_properties, "Absorb nested of #{member.inspect} into #{mapping.name}" do
414
+ # In the DocGraph composition, either absorb the contents or devolve a triple
415
+ if @triple_composites[member.child_role.object_type]
416
+ trace :docgraph_triple, "Handle #{member.inspect} as a semantic triple"
417
+ member.retract
418
+ mapping.re_rank
419
+ return
420
+ end
421
+
422
+ # This is a nested structure, annotate as Nested, flip the member and absorb all
423
+ @constellation.Nesting(member, 0, index_role: member.child_role)
424
+
425
+ # member.flip!
426
+ child_object_type = member.child_role.object_type
427
+ child_mapping = @binary_mappings[child_object_type]
428
+ absorb_all(member, child_mapping, paths, stack)
429
+ end
430
+ end
431
+
432
+ # May be overridden in subclasses
433
+ def prefer_natural_key building_natural_key, source_composite, target_composite
434
+ false
435
+ end
436
+
437
+ # Augment the mapping with copies of the children of the "from" mapping.
438
+ # At the top level, no "from" is given and the children already exist
439
+ def absorb_all mapping, from, paths = {}, stack = []
440
+ trace :docgraph_properties, "Absorbing all from #{from.inspect} into #{mapping.name}" do
441
+ top_level = mapping == from
442
+
443
+ pcs = []
444
+ newpaths = {}
445
+ if mapping.composite || mapping.full_absorption
446
+ pcs = find_uniqueness_constraints(mapping)
447
+
448
+ # Don't build an index from the same PresenceConstraint twice on the same composite (e.g. for a subtype)
449
+ existing_pcs = mapping.root.all_access_path.select{|ap| MM::Index === ap}.map(&:presence_constraint)
450
+ newpaths = make_new_paths(mapping, paths.keys+existing_pcs, pcs)
451
+ end
452
+
453
+ from.re_rank
454
+ substack = stack + [from.object_type]
455
+ ordered = from.all_member.sort_by(&:ordinal)
456
+ ordered.each do |member|
457
+ trace :docgraph_properties, "... considering #{member.child_role.object_type.name}"
458
+
459
+ # Only proceed if there is no absorption loop and we are not jumping to another document
460
+ if !absorption_loop(member, substack) && !@document_composites[member.child_role.object_type]
461
+ unless top_level # Top-level members are already instantiated
462
+ member = member.fork_to_new_parent(mapping)
463
+ end
464
+ rel = paths.merge(relevant_paths(newpaths, member))
465
+ augment_paths(rel, member)
466
+
467
+ if member.is_a?(MM::Absorption) && !member.forward_absorption && member.parent_role.is_unique && member.child_role.object_type.is_a?(MM::EntityType)
468
+ # Only forward absorptions here please...
469
+ absorb_subdoc(mapping, member, rel, substack)
470
+ elsif member.is_a?(MM::Absorption) && (member.forward_absorption || !member.parent_role.is_unique) # && top_level
471
+ absorb_nested(mapping, member, rel, substack)
472
+ end
473
+ end
474
+ end
475
+
476
+ # Clean up if mapping does not have any members
477
+ if mapping.all_member.size == 0
478
+ mapping_parent = mapping.parent
479
+ mapping.retract
480
+ mapping_parent.re_rank
481
+ end
482
+
483
+ newpaths.values.select{|ix| ix.all_index_field.size == 0}.each(&:retract)
484
+ end
485
+ end
486
+
487
+ def absorption_loop(absorption, stack)
488
+ trace :docgraph_properties, "Stack is #{stack.map{|ot| ot.name} * ', '}"
489
+ result = stack.any? {|ot| ot == absorption.child_role.object_type}
490
+ trace :docgraph_properties, "absorption child is #{absorption.child_role.object_type.name}, loop is #{result}"
491
+ result
492
+ end
493
+
494
+ # Find all PresenceConstraints to index the object in this Mapping
495
+ def find_uniqueness_constraints mapping
496
+ return [] unless mapping.object_type.is_a?(MM::EntityType)
497
+
498
+ start_roles =
499
+ mapping.
500
+ object_type.
501
+ all_role_transitive. # Includes objectification roles for objectified fact types
502
+ select do |role|
503
+ (role.is_unique || # Must be unique on near role
504
+ role.fact_type.is_unary) && # Or be a unary role
505
+ !(role.fact_type.is_a?(MM::TypeInheritance) && role == role.fact_type.supertype_role) # allow roles as subtype
506
+ end.
507
+ map(&:counterpart). # (Same role if it's a unary)
508
+ compact. # Ignore nil counterpart of a role in an n-ary
509
+ map(&:base_role). # In case it's a link fact type
510
+ uniq
511
+
512
+ pcs =
513
+ start_roles.
514
+ flat_map(&:all_role_ref). # All role_refs
515
+ map(&:role_sequence). # The role_sequence
516
+ uniq.
517
+ flat_map(&:all_presence_constraint).
518
+ uniq.
519
+ reject do |pc|
520
+ pc.max_frequency != 1 || # Must be unique
521
+ pc.enforcement || # and alethic
522
+ pc.role_sequence.all_role_ref.detect do |rr|
523
+ !start_roles.include?(rr.role) # and span only valid roles
524
+ end || # and not be the full absorption path
525
+ ( # Reject a constraint that caused full absorption
526
+ pc.role_sequence.all_role_ref.size == 1 and
527
+ mapping.is_a?(MM::Absorption) and
528
+ fa = mapping.full_absorption and
529
+ pc.role_sequence.all_role_ref.single.role.base_role == fa.absorption.parent_role.base_role
530
+ )
531
+ end # Alethic uniqueness constraint on far end
532
+
533
+ non_absorption_pcs = pcs.reject do |pc|
534
+ # An absorption PC is a PC that covers some role that is involved in a FullAbsorption
535
+ full_absorptions =
536
+ pc.
537
+ role_sequence.
538
+ all_role_ref.
539
+ map(&:role).
540
+ flat_map do |role|
541
+ (role.all_absorption_as_parent_role.to_a + role.all_absorption_as_child_role.to_a).
542
+ select do |abs|
543
+ abs.full_absorption && abs.full_absorption.composition == @composition
544
+ end
545
+ end
546
+ full_absorptions.size > 0
547
+ end
548
+ pcs = non_absorption_pcs
549
+
550
+ trace :docgraph_paths, "Uniqueness Constraints for #{mapping.object_type.name}" do
551
+ pcs.each do |pc|
552
+ trace :docgraph_paths, "#{pc.describe.inspect}#{pc.is_preferred_identifier ? ' (PI)' : ''}"
553
+ end
554
+ end
555
+
556
+ pcs
557
+ end
558
+
559
+ def make_new_paths mapping, existing_pcs, pcs
560
+ newpaths = {}
561
+ new_pcs = pcs-existing_pcs
562
+ trace :docgraph_paths, "Adding #{new_pcs.size} new indices for presence constraints on #{mapping.inspect}" do
563
+ new_pcs.each do |pc|
564
+ newpaths[pc] = index = @constellation.Index(:new, composite: mapping.root, is_unique: true, presence_constraint: pc)
565
+ if mapping.object_type.preferred_identifier == pc and
566
+ !@composition.all_full_absorption[mapping.object_type] and
567
+ !mapping.root.natural_index
568
+ mapping.root.natural_index = index
569
+ mapping.root.primary_index ||= index # Not if we have a surrogate already
570
+ end
571
+ trace :docgraph_paths, "Added new index #{index.inspect} for #{pc.describe} on #{pc.role_sequence.all_role_ref.map(&:role).map(&:fact_type).map(&:default_reading).inspect}"
572
+ end
573
+ end
574
+ newpaths
575
+ end
576
+
577
+ def relevant_paths path_hash, component
578
+ rel = {} # REVISIT: return a hash subset of path_hash containing paths relevant to this component
579
+ case component
580
+ when MM::Absorption
581
+ role = component.child_role.base_role
582
+ when MM::Indicator
583
+ role = component.role
584
+ else
585
+ return rel # Can't participate in an AccessPath
586
+ end
587
+
588
+ path_hash.each do |pc, path|
589
+ next unless pc.role_sequence.all_role_ref.detect{|rr| rr.role == role}
590
+ rel[pc] = path
591
+ end
592
+ rel
593
+ end
594
+
595
+ def augment_paths paths, mapping
596
+ return unless MM::Indicator === mapping || MM::ValueType === mapping.object_type
597
+
598
+ if MM::ValueField === mapping && mapping.parent.composite # ValueType that's a composite (table) by itself
599
+ # This AccessPath has exactly one field and no presence constraint, so just make the index.
600
+ composite = mapping.parent.composite
601
+ paths[nil] =
602
+ index = @constellation.Index(:new, composite: mapping.root, is_unique: true, presence_constraint: nil, composite_as_natural_index: composite)
603
+ composite.primary_index ||= index
604
+ end
605
+
606
+ paths.each do |pc, path|
607
+ trace :docgraph_paths, "Adding access path #{mapping.inspect} to #{path.inspect}" do
608
+ case path
609
+ when MM::Index
610
+ @constellation.IndexField(access_path: path, ordinal: path.all_index_field.size, component: mapping)
611
+ when MM::ForeignKey
612
+ @constellation.ForeignKeyField(foreign_key: path, ordinal: path.all_foreign_key_field.size, component: mapping)
613
+ end
614
+ end
615
+ end
616
+ end
617
+
618
+ # Make a new Absorption in the reverse direction from the one given
619
+ def mirror absorption, parent
620
+ @constellation.fork(
621
+ absorption,
622
+ guid: :new,
623
+ object_type: absorption.parent_role.object_type,
624
+ parent: parent,
625
+ parent_role: absorption.child_role,
626
+ child_role: absorption.parent_role,
627
+ ordinal: 0,
628
+ name: role_name(absorption.parent_role)
629
+ )
630
+ end
631
+
632
+ # A candidate is a Mapping of an object type which may become a Composition (a table, in docgraph-speak)
633
+ class Candidate
634
+ attr_reader :mapping, :is_document, :is_triple, :is_tentative
635
+ attr_accessor :full_absorption
636
+
637
+ def initialize compositor, mapping
638
+ @compositor = compositor
639
+ @mapping = mapping
640
+ end
641
+
642
+ def object_type
643
+ @mapping.object_type
644
+ end
645
+
646
+ # References from us are things we can own (non-Mappings) or have a unique forward absorption for
647
+ def references_from
648
+ @mapping.all_member.select{|m| !m.is_a?(MM::Absorption) or !m.forward_absorption && m.parent_role.is_unique }
649
+ end
650
+ alias_method :rf, :references_from
651
+
652
+ # References to us are reverse absorptions where the forward absorption can absorb us
653
+ def references_to
654
+ @mapping.all_member.select{|m| m.is_a?(MM::Absorption) and f = m.forward_absorption and f.parent_role.is_unique}
655
+ end
656
+ alias_method :rt, :references_to
657
+
658
+ def has_references
659
+ @mapping.all_member.select{|m| m.is_a?(MM::Absorption) }
660
+ end
661
+
662
+ def definitely_not_document
663
+ @is_tentative = @is_document = false
664
+ end
665
+
666
+ def definitely_document
667
+ @is_tentative = false
668
+ @is_document = true
669
+ @is_triple = false
670
+ end
671
+
672
+ def definitely_not_triple
673
+ @is_tentative = @is_triple = false
674
+ end
675
+
676
+ def definitely_triple
677
+ @is_tentative = false
678
+ @is_triple = true
679
+ @is_document = false
680
+ end
681
+
682
+ def probably_not_document
683
+ @is_tentative = true
684
+ @is_document = false
685
+ end
686
+
687
+ def probably_document
688
+ @is_tentative = @is_document = true
689
+ end
690
+
691
+ def assign_default composition
692
+ o = object_type
693
+ if o.is_separate
694
+ trace :docgraph_defaults, "#{o.name} is a document because it's declared independent or separate"
695
+ definitely_document
696
+ return
697
+ end
698
+
699
+ if o.concept.all_concept_annotation.detect{|ca| ca.mapping_annotation =~ TRIPLE_ANNOTATION}
700
+ trace :docgraph_defaults, "#{o.name} is a triple because it's declared triple"
701
+ definitely_triple
702
+ return
703
+ end
704
+
705
+ case o
706
+ when MM::ValueType
707
+ if o.is_auto_assigned
708
+ trace :docgraph_defaults, "#{o.name} is not a document because it is auto assigned"
709
+ definitely_not_document
710
+ elsif references_from.size > 0
711
+ trace :docgraph_defaults, "#{o.name} is a document because it has references to absorb"
712
+ definitely_document
713
+ else
714
+ trace :docgraph_defaults, "#{o.name} is not a document because it will be absorbed wherever needed"
715
+ definitely_not_document
716
+ end
717
+
718
+ when MM::EntityType
719
+ if references_to.empty? and
720
+ !references_from.detect do |absorption| # detect whether anything can absorb this entity type
721
+ absorption.is_a?(MM::Mapping) && absorption.parent_role.is_unique # DG && absorption.child_role.is_unique
722
+ end
723
+ trace :docgraph_defaults, "#{o.name} is a document because it has nothing to absorb it"
724
+ definitely_document
725
+ return
726
+ end
727
+
728
+ # its a triple if this is an objectified fact type that has a uniqueness constraint of size = 2
729
+ if o.fact_type
730
+ # List the UCs on this fact type:
731
+ all_uniqueness_constraints =
732
+ o.fact_type.all_role.map do |fact_role|
733
+ fact_role.all_role_ref.map do |rr|
734
+ rr.role_sequence.all_presence_constraint.select { |pc| pc.max_frequency == 1 }
735
+ end
736
+ end.flatten.uniq
737
+
738
+ if all_uniqueness_constraints.detect do |uc|
739
+ (arr = uc.role_sequence.all_role_ref).size == 2 and arr[0].role.object_type.is_a?(MM::EntityType) and arr[1].role.object_type.is_a?(MM::EntityType)
740
+ end
741
+ trace :docgraph_defaults, "#{o.name} is a triple because is an objectified fact type with uniqueness contraint of 2"
742
+ definitely_triple
743
+ return
744
+ end
745
+ end
746
+
747
+ if !o.supertypes.empty?
748
+ # We know that this entity type is not a separate or partitioned subtype, so a supertype that can absorb us does
749
+ identifying_fact_type = o.all_type_inheritance_as_subtype.detect{|ti| ti.provides_identification}
750
+ if identifying_fact_type
751
+ fact_type = identifying_fact_type
752
+ else
753
+ if o.all_type_inheritance_as_subtype.size > 1
754
+ trace :docgraph_defaults, "REVISIT: #{o.name} cannot be absorbed into a supertype that doesn't also absorb all our other supertypes (or is absorbed into one of its supertypes that does)"
755
+ end
756
+ fact_type = o.all_type_inheritance_as_subtype.to_a[0]
757
+ end
758
+
759
+ absorbing_ref = mapping.all_member.detect{|m| m.is_a?(MM::Absorption) && m.child_role.fact_type == fact_type}
760
+
761
+ absorbing_ref = absorbing_ref.flip! if absorbing_ref.reverse_absorption # We were forward, but the other end must be
762
+ absorbing_ref = absorbing_ref.forward_absorption
763
+ self.full_absorption =
764
+ o.constellation.FullAbsorption(composition: composition, absorption: absorbing_ref, object_type: o)
765
+ trace :docgraph_defaults, "Supertype #{fact_type.supertype_role.name} fully absorbs subtype #{o.name} via #{absorbing_ref.inspect}"
766
+ definitely_not_document
767
+ return
768
+ end # subtype
769
+
770
+ # If the preferred_identifier consists of a ValueType that's auto-assigned,
771
+ # that can only happen in one document, which controls the sequence.
772
+ auto_assigned_identifying_role_player = nil
773
+ pi_role_refs = o.preferred_identifier.role_sequence.all_role_ref
774
+ if pi_role_refs.size == 1 and
775
+ rr = pi_role_refs.single and
776
+ (v = rr.role.object_type).is_a?(MM::ValueType) and
777
+ v.is_auto_assigned == 'commit'
778
+ auto_assigned_identifying_role_player = v
779
+ end
780
+ if (@compositor.options['single_sequence'] || references_to.size > 1) and auto_assigned_identifying_role_player # Can be absorbed in more than one place
781
+ trace :docgraph_defaults, "#{o.name} must be a document to support its auto-assigned identifier #{auto_assigned_identifying_role_player.name}"
782
+ definitely_document
783
+ return
784
+ end
785
+
786
+ trace :docgraph_defaults, "#{o.name} is initially presumed to be a document"
787
+ probably_document
788
+
789
+ end # case
790
+ end
791
+
792
+ end
793
+
794
+ end
795
+
796
+ publish_compositor(DocGraph)
797
+ end
798
+ end