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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/activefacts-compositions.gemspec +6 -5
- data/bin/afcomp +192 -0
- data/bin/schema_compositor +26 -11
- data/lib/activefacts/compositions/binary.rb +4 -0
- data/lib/activefacts/compositions/compositor.rb +79 -7
- data/lib/activefacts/compositions/datavault.rb +308 -110
- data/lib/activefacts/compositions/docgraph.rb +798 -0
- data/lib/activefacts/compositions/relational.rb +8 -8
- data/lib/activefacts/compositions/staging.rb +3 -2
- data/lib/activefacts/compositions/version.rb +1 -1
- data/lib/activefacts/generator/doc/cwm.rb +11 -17
- data/lib/activefacts/generator/oo.rb +2 -3
- data/lib/activefacts/generator/rails/models.rb +2 -3
- data/lib/activefacts/generator/rails/schema.rb +2 -3
- data/lib/activefacts/generator/ruby.rb +1 -2
- data/lib/activefacts/generator/sql.rb +29 -15
- data/lib/activefacts/generator/summary.rb +22 -17
- data/lib/activefacts/generator/transgen.rb +144 -0
- data/lib/activefacts/generator/validate.rb +3 -3
- metadata +59 -21
@@ -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
|