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