activefacts-compositions 1.9.10 → 1.9.12

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,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