activefacts-compositions 1.9.6 → 1.9.8
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/Rakefile +33 -0
- data/activefacts-compositions.gemspec +3 -3
- data/bin/schema_compositor +142 -85
- data/lib/activefacts/compositions/binary.rb +19 -15
- data/lib/activefacts/compositions/compositor.rb +126 -125
- data/lib/activefacts/compositions/constraints.rb +74 -54
- data/lib/activefacts/compositions/datavault.rb +545 -0
- data/lib/activefacts/compositions/names.rb +58 -58
- data/lib/activefacts/compositions/relational.rb +801 -692
- data/lib/activefacts/compositions/traits/rails.rb +180 -0
- data/lib/activefacts/compositions/version.rb +1 -1
- data/lib/activefacts/generator/doc/css/ldm.css +45 -0
- data/lib/activefacts/generator/doc/cwm.rb +764 -0
- data/lib/activefacts/generator/doc/glossary.rb +473 -0
- data/lib/activefacts/generator/doc/graphviz.rb +134 -0
- data/lib/activefacts/generator/doc/ldm.rb +698 -0
- data/lib/activefacts/generator/oo.rb +130 -124
- data/lib/activefacts/generator/rails/models.rb +237 -0
- data/lib/activefacts/generator/rails/schema.rb +273 -0
- data/lib/activefacts/generator/ruby.rb +75 -67
- data/lib/activefacts/generator/sql.rb +333 -351
- data/lib/activefacts/generator/sql/server.rb +100 -39
- data/lib/activefacts/generator/summary.rb +67 -59
- data/lib/activefacts/generator/validate.rb +19 -134
- metadata +18 -15
@@ -9,75 +9,95 @@ module ActiveFacts
|
|
9
9
|
module Metamodel
|
10
10
|
class Composition
|
11
11
|
def retract_constraint_classifications
|
12
|
-
|
12
|
+
all_composite.each(&:retract_constraint_classifications)
|
13
13
|
end
|
14
14
|
|
15
15
|
def classify_constraints
|
16
|
-
|
17
|
-
|
16
|
+
retract_constraint_classifications
|
17
|
+
all_composite.each(&:classify_constraints)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class Component
|
22
|
+
def gather_constraints all_composite_roles = [], all_composite_constraints = [], constraints_by_leaf = {}
|
23
|
+
all_role.each do |role|
|
24
|
+
all_composite_roles << role
|
25
|
+
role.all_constraint.each do |constraint|
|
26
|
+
# Exclude single-role mandatory constraints and all uniqueness constraints:
|
27
|
+
next if constraint.is_a?(PresenceConstraint) and
|
28
|
+
constraint.max_frequency == 1 ||
|
29
|
+
(constraint.role_sequence.all_role_ref.size == 1 && constraint.min_frequency == 1 && constraint.is_mandatory)
|
30
|
+
all_composite_constraints << constraint
|
31
|
+
(constraints_by_leaf[self] ||= []) << constraint
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
gather_nested_constraints all_composite_roles, all_composite_constraints, constraints_by_leaf
|
36
|
+
end
|
37
|
+
|
38
|
+
def gather_nested_constraints all_composite_roles, all_composite_constraints, constraints_by_leaf
|
39
|
+
all_member.each do |member|
|
40
|
+
member.gather_constraints all_composite_roles, all_composite_constraints, constraints_by_leaf
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class Absorption
|
46
|
+
def gather_nested_constraints all_composite_roles, all_composite_constraints, constraints_by_leaf
|
47
|
+
return if foreign_key # This has gone far enough!
|
48
|
+
super
|
18
49
|
end
|
19
50
|
end
|
20
51
|
|
21
52
|
class Composite
|
22
53
|
def retract_constraint_classifications
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
54
|
+
all_spanning_constraint.to_a.each(&:retract)
|
55
|
+
all_local_constraint.to_a.each(&:retract)
|
56
|
+
mapping.all_leaf.each do |component|
|
57
|
+
component.all_leaf_constraint.to_a.each(&:retract)
|
58
|
+
end
|
28
59
|
end
|
29
60
|
|
30
61
|
def classify_constraints
|
31
|
-
|
32
|
-
|
33
|
-
# Categorise and index all constraints not already baked-in to the composition
|
34
|
-
all_composite_roles = []
|
35
|
-
all_composite_constraints = []
|
36
|
-
constraints_by_leaf = {}
|
37
|
-
leaves.each do |leaf|
|
38
|
-
all_composite_roles += leaf.path.flat_map(&:all_role) # May be non-unique, fix later
|
39
|
-
leaf.all_role.each do |role|
|
40
|
-
role.all_constraint.each do |constraint|
|
41
|
-
if constraint.is_a?(PresenceConstraint)
|
42
|
-
# Exclude single-role mandatory constraints and all uniqueness constraints:
|
43
|
-
if constraint.role_sequence.all_role_ref.size == 1 && constraint.min_frequency == 1 && constraint.is_mandatory or
|
44
|
-
constraint.max_frequency == 1
|
45
|
-
next
|
46
|
-
end
|
47
|
-
end
|
48
|
-
all_composite_constraints << constraint
|
49
|
-
(constraints_by_leaf[leaf] ||= []) << constraint
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
62
|
+
leaves = mapping.all_leaf
|
53
63
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
64
|
+
# Categorise and index all constraints not already baked-in to the composition
|
65
|
+
# We recurse down the hierarchy, stopping at any foreign keys
|
66
|
+
all_composite_roles = []
|
67
|
+
all_composite_constraints = []
|
68
|
+
constraints_by_leaf = {}
|
69
|
+
mapping.gather_constraints all_composite_roles, all_composite_constraints, constraints_by_leaf
|
70
|
+
all_composite_roles.uniq!
|
71
|
+
all_composite_constraints.uniq!
|
61
72
|
|
62
|
-
|
63
|
-
|
64
|
-
|
73
|
+
# Spanning constraints constrain some role outside this composite:
|
74
|
+
spanning_constraints =
|
75
|
+
all_composite_constraints.select do |constraint|
|
76
|
+
(constraint.all_constrained_role-all_composite_roles).size > 0
|
77
|
+
end
|
78
|
+
spanning_constraints.each do |spanning_constraint|
|
79
|
+
constellation.SpanningConstraint(composite: self, spanning_constraint: spanning_constraint)
|
80
|
+
end
|
65
81
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
82
|
+
# Local and leaf constraints are what remains. Extract the leaf constraints:
|
83
|
+
local_constraints = all_composite_constraints - spanning_constraints
|
84
|
+
leaves.each do |leaf|
|
85
|
+
# Find any constraints that affect just this leaf:
|
86
|
+
leaf_constraints =
|
87
|
+
leaf.path.flat_map{|component| Array(constraints_by_leaf[component]) }.
|
88
|
+
select do |constraint|
|
89
|
+
# Does this constraint constrain only this leaf?
|
90
|
+
(constraint.all_constrained_role - leaf.path.flat_map(&:all_role)).size == 0
|
91
|
+
end
|
92
|
+
leaf_constraints.each do |leaf_constraint|
|
93
|
+
constellation.LeafConstraint(component: leaf, leaf_constraint: leaf_constraint)
|
94
|
+
end
|
95
|
+
local_constraints -= leaf_constraints
|
96
|
+
end
|
77
97
|
|
78
|
-
|
79
|
-
|
80
|
-
|
98
|
+
local_constraints.each do |local_constraint|
|
99
|
+
constellation.LocalConstraint(composite: self, local_constraint: local_constraint)
|
100
|
+
end
|
81
101
|
|
82
102
|
end
|
83
103
|
end
|
@@ -0,0 +1,545 @@
|
|
1
|
+
#
|
2
|
+
# ActiveFacts Compositions, DataVault Compositor.
|
3
|
+
#
|
4
|
+
# Computes a Data Vault schema.
|
5
|
+
#
|
6
|
+
# Copyright (c) 2015 Clifford Heath. Read the LICENSE file.
|
7
|
+
#
|
8
|
+
require "activefacts/compositions/relational"
|
9
|
+
|
10
|
+
module ActiveFacts
|
11
|
+
module Compositions
|
12
|
+
class DataVault < Relational
|
13
|
+
public
|
14
|
+
def self.options
|
15
|
+
{
|
16
|
+
reference: ['Boolean', "Emit the reference (static) tables as well. Default is to omit them"],
|
17
|
+
datestamp: ['String', "Use this data type for date stamps"],
|
18
|
+
id: ['String', "Append this to data vault surrogate keys (default VID)"],
|
19
|
+
hubname: ['String', "Suffix or pattern for naming hub tables. Include a + to insert the name. Default 'HUB'"],
|
20
|
+
linkname: ['String', "Suffix or pattern for naming link tables. Include a + to insert the name. Default 'LINK'"],
|
21
|
+
satname: ['String', "Suffix or pattern for naming satellite tables. Include a + to insert the name. Default 'SAT'"],
|
22
|
+
refname: ['String', "Suffix or pattern for naming reference tables. Include a + to insert the name. Default '+'"],
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize constellation, name, options = {}
|
27
|
+
# Extract recognised options:
|
28
|
+
@option_reference = options.delete('reference')
|
29
|
+
@option_datestamp = options.delete('datestamp')
|
30
|
+
@option_id = ' ' + (options.delete('id') || 'VID')
|
31
|
+
@option_hub_name = options.delete('hubname') || 'HUB'
|
32
|
+
@option_hub_name.sub!(/^/,'+ ') unless @option_hub_name =~ /\+/
|
33
|
+
@option_link_name = options.delete('linkname') || 'LINK'
|
34
|
+
@option_link_name.sub!(/^/,'+ ') unless @option_link_name =~ /\+/
|
35
|
+
@option_sat_name = options.delete('satname') || 'SAT'
|
36
|
+
@option_sat_name.sub!(/^/,'+ ') unless @option_sat_name =~ /\+/
|
37
|
+
@option_ref_name = options.delete('refname') || 'SAT'
|
38
|
+
@option_ref_name.sub!(/^/,'+ ') unless @option_ref_name =~ /\+/
|
39
|
+
|
40
|
+
super constellation, name, options
|
41
|
+
|
42
|
+
@option_surrogates = true # Always inject surrogates regardless of superclass
|
43
|
+
end
|
44
|
+
|
45
|
+
# We need to find links that need surrogate keys before we inject the surrogates
|
46
|
+
def inject_surrogates
|
47
|
+
classify_composites
|
48
|
+
|
49
|
+
super
|
50
|
+
end
|
51
|
+
|
52
|
+
def composite_is_reference composite
|
53
|
+
object_type = composite.mapping.object_type
|
54
|
+
object_type.concept.all_concept_annotation.detect{|ca| ca.mapping_annotation == 'static'} or
|
55
|
+
!object_type.is_a?(ActiveFacts::Metamodel::EntityType)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Data Vaults need a surrogate key on every Hub and Link.
|
59
|
+
# Don't add a surrogate on a Reference table!
|
60
|
+
def needs_surrogate(composite)
|
61
|
+
return false if composite_is_reference(composite)
|
62
|
+
|
63
|
+
# REVISIT: The following is debatable. If the natural primary key is an ok surrogate, should we inject another?
|
64
|
+
return true if @non_reference_composites.include?(composite)
|
65
|
+
|
66
|
+
super
|
67
|
+
end
|
68
|
+
|
69
|
+
# Change the default extension from our superclass':
|
70
|
+
def inject_surrogate composite, extension = @option_id
|
71
|
+
super
|
72
|
+
end
|
73
|
+
|
74
|
+
def classify_composites
|
75
|
+
detect_reference_tables
|
76
|
+
|
77
|
+
trace :datavault, "Classify non-reference tables into hubs and links" do
|
78
|
+
# Make an initial determination, then adjust for foreign keys to links afterwards
|
79
|
+
@key_structure = {}
|
80
|
+
@link_composites, @hub_composites =
|
81
|
+
@non_reference_composites.
|
82
|
+
sort_by{|c| c.mapping.name}.
|
83
|
+
partition do |composite|
|
84
|
+
trace :datavault, "Decide whether #{composite.mapping.name} is a link or a hub" do
|
85
|
+
@key_structure[composite] =
|
86
|
+
mapped_to =
|
87
|
+
composite_key_structure composite
|
88
|
+
|
89
|
+
# It's a Link if the preferred identifier includes more than non_reference_composite.
|
90
|
+
mapped_to.compact.size > 1
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
trace :datavault, "Checking for foreign keys that reference links" do
|
95
|
+
# Links may never be the target of a foreign key.
|
96
|
+
# Any such links must be defined as hubs instead.
|
97
|
+
@links_as_hubs = {}
|
98
|
+
fk_dependencies_by_target = {}
|
99
|
+
fk_dependencies_by_source = {}
|
100
|
+
(@hub_composites+@link_composites).
|
101
|
+
each do |composite|
|
102
|
+
target_composites = enumerate_foreign_keys composite.mapping
|
103
|
+
target_composites.each do |target_composite|
|
104
|
+
next if @reference_composites.include?(target_composite)
|
105
|
+
(fk_dependencies_by_target[target_composite] ||= []) << composite
|
106
|
+
(fk_dependencies_by_source[composite] ||= []) << target_composite
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
fk_dependencies_by_target.keys.each do |target_composite|
|
111
|
+
if @link_composites.delete(target_composite)
|
112
|
+
trace :datavault, "Link #{target_composite.inspect} must be a hub because foreign keys reference it"
|
113
|
+
@hub_composites << target_composite
|
114
|
+
@links_as_hubs[target_composite] = true
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
begin
|
119
|
+
converted =
|
120
|
+
@link_composites.select do |composite|
|
121
|
+
targets = fk_dependencies_by_source[composite]
|
122
|
+
id_targets = composite_key_structure(composite).compact
|
123
|
+
next if targets.size == id_targets.size
|
124
|
+
trace :datavault, "Link #{composite.mapping.name} must be a hub because it has non-identifying FK references"
|
125
|
+
@link_composites.delete(composite)
|
126
|
+
@hub_composites << composite
|
127
|
+
@links_as_hubs[composite] = true
|
128
|
+
end
|
129
|
+
end while converted.size > 0
|
130
|
+
|
131
|
+
end
|
132
|
+
|
133
|
+
# Note: We may still have hubs whose identifiers contain foreign keys to one or more other hubs.
|
134
|
+
# REVISIT: These foreign keys will be deleted so these hubs stand alone,
|
135
|
+
# but have been re-instated as new links to the referenced hubs.
|
136
|
+
end
|
137
|
+
|
138
|
+
trace :datavault_classification!, "Data Vault classification of composites:" do
|
139
|
+
trace :datavault, "Reference: #{@reference_composites.map(&:mapping).map(&:object_type).map(&:name)*', '}"
|
140
|
+
trace :datavault, "Hub: #{@hub_composites.map(&:mapping).map(&:object_type).map(&:name)*', '}"
|
141
|
+
trace :datavault, "Link: #{@link_composites.map(&:mapping).map(&:object_type).map(&:name)*', '}"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def detect_reference_tables
|
146
|
+
initial_composites = @composition.all_composite.to_a
|
147
|
+
@reference_composites, @non_reference_composites =
|
148
|
+
initial_composites.partition { |composite| composite_is_reference(composite) }
|
149
|
+
end
|
150
|
+
|
151
|
+
def devolve_all
|
152
|
+
delete_reference_table_foreign_keys
|
153
|
+
|
154
|
+
# For each hub and link, move each non-identifying member
|
155
|
+
# to a new satellite or promote it to a new link.
|
156
|
+
|
157
|
+
@non_reference_composites.
|
158
|
+
each do |composite|
|
159
|
+
devolve composite
|
160
|
+
end
|
161
|
+
|
162
|
+
rename_parents
|
163
|
+
|
164
|
+
unless @option_reference
|
165
|
+
if trace :reference_retraction
|
166
|
+
# Add a logger so we can trace the resultant retractions:
|
167
|
+
@constellation.loggers << proc do |*args|
|
168
|
+
trace :reference_retraction, args.inspect
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
@reference_composites.each do |rc|
|
173
|
+
trace :reference_retraction, "Retracting #{rc.inspect}" do
|
174
|
+
rc.retract
|
175
|
+
end
|
176
|
+
end
|
177
|
+
@reference_composites = []
|
178
|
+
|
179
|
+
@constellation.loggers.pop if trace :reference_retraction
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def delete_reference_table_foreign_keys
|
184
|
+
trace :datavault, "Delete foreign keys to reference tables" do
|
185
|
+
# Delete all foreign keys to reference tables
|
186
|
+
@reference_composites.each do |composite|
|
187
|
+
composite.all_foreign_key_as_target_composite.each(&:retract)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def prefer_natural_key building_natural_key, source_composite, target_composite
|
193
|
+
return false if building_natural_key && @link_composites.include?(source_composite)
|
194
|
+
building_natural_key && @hub_composites.include?(target_composite)
|
195
|
+
end
|
196
|
+
|
197
|
+
def composite_key_structure composite
|
198
|
+
# We know that composite.mapping.object_type is an EntityType because all ValueType composites are reference tables
|
199
|
+
|
200
|
+
object_type = composite.mapping.object_type
|
201
|
+
mapped_to =
|
202
|
+
object_type.preferred_identifier.role_sequence.all_role_ref_in_order.map do |role_ref|
|
203
|
+
player = role_ref.role.object_type
|
204
|
+
next nil if player == object_type && role_ref.role.fact_type.all_role.size == 1 # Unaries.
|
205
|
+
candidate = @candidates[player]
|
206
|
+
next nil unless candidate
|
207
|
+
# Take care of full absorption
|
208
|
+
while candidate.full_absorption
|
209
|
+
candidate = candidate.full_absorption.composition
|
210
|
+
end
|
211
|
+
@non_reference_composites.include?(c = candidate.mapping.composite) ? c : nil
|
212
|
+
end
|
213
|
+
|
214
|
+
trace :datavault, "Preferred identifier for #{composite.mapping.name} encloses foreign keys to #{mapped_to.inspect}" unless mapped_to.compact.empty?
|
215
|
+
|
216
|
+
number_of_keys = mapped_to.compact.size
|
217
|
+
number_of_values = mapped_to.size-number_of_keys
|
218
|
+
trace :datavault_classify,
|
219
|
+
if number_of_keys > 1
|
220
|
+
# Links have more than one FK to a hub in their key
|
221
|
+
"Link #{composite.mapping.name} links #{mapped_to.compact.inspect} with #{number_of_values} values"
|
222
|
+
elsif number_of_keys == 1 && number_of_values > 0
|
223
|
+
# This is a new hub with a composite key - but we will have to eliminate the foreign key to the base hub
|
224
|
+
"Augmented Hub #{composite.mapping.name} has a hub link to #{mapped_to.compact[0].inspect} and #{number_of_values} values"
|
225
|
+
elsif number_of_keys == 1
|
226
|
+
# This is a new hub with a single-part key that references another hub.
|
227
|
+
"Dependent Hub #{composite.mapping.name} is identified by another hub: #{mapped_to.compact[0].inspect}"
|
228
|
+
else
|
229
|
+
"Hub #{composite.mapping.name} has #{mapped_to.size} parts in its key"
|
230
|
+
end
|
231
|
+
|
232
|
+
mapped_to
|
233
|
+
end
|
234
|
+
|
235
|
+
# For each member of this composite, decide whether to devolve it to a satellite
|
236
|
+
# or to a new link. If it goes to a link that's still part of this natural key,
|
237
|
+
# we need to leave that key intact, but remove the foreign key it entails.
|
238
|
+
#
|
239
|
+
# New links and satellites get new fields for the load date-time and a
|
240
|
+
# references to the surrogate(s) on the hub or link, and add an index over
|
241
|
+
# those two fields.
|
242
|
+
def devolve composite, devolve_links = true
|
243
|
+
trace :datavault?, "Devolving non-identifying fields for #{composite.inspect}" do
|
244
|
+
# Find the members of this mapping that contain identifying leaves:
|
245
|
+
pi = composite.primary_index
|
246
|
+
ni = composite.natural_index
|
247
|
+
identifiers =
|
248
|
+
(Array(pi ? pi.all_index_field : nil) +
|
249
|
+
Array(ni ? ni.all_index_field : nil)).
|
250
|
+
map{|ixf| ixf.component.path[1]}.
|
251
|
+
uniq
|
252
|
+
|
253
|
+
satellites = {}
|
254
|
+
is_link = @link_composites.include?(composite) || @links_as_hubs.include?(composite)
|
255
|
+
composite.mapping.all_member.to_a.each do |member|
|
256
|
+
|
257
|
+
# Any member that is the absorption of a foreign key to a hub or link
|
258
|
+
# (which is all, since we removed FKs to reference tables)
|
259
|
+
# must be converted to a Mapping for a new Entity Type that notionally
|
260
|
+
# objectifies the absorbed fact type. This Mapping is a new link composite.
|
261
|
+
if devolve_links && member.is_a?(MM::Absorption) && member.foreign_key
|
262
|
+
next if is_link
|
263
|
+
devolve_absorption_to_link member, identifiers.include?(member)
|
264
|
+
next
|
265
|
+
end
|
266
|
+
|
267
|
+
# If this member is in the natural or surrogate key, leave it there
|
268
|
+
# REVISIT: But if it is an FK to another hub, devolve it to a link as well.
|
269
|
+
next if identifiers.include?(member)
|
270
|
+
|
271
|
+
# We may absorb a subtype that has no contents. There's no point moving these to a satellite.
|
272
|
+
next if is_empty_inheritance member
|
273
|
+
|
274
|
+
satellite_name = name_satellite member
|
275
|
+
satellite = satellites[satellite_name]
|
276
|
+
unless satellite
|
277
|
+
satellite =
|
278
|
+
satellites[satellite_name] =
|
279
|
+
create_satellite satellite_name, composite
|
280
|
+
end
|
281
|
+
|
282
|
+
devolve_member_to_satellite satellite, member
|
283
|
+
end
|
284
|
+
composite.mapping.re_rank
|
285
|
+
|
286
|
+
if @hub_composites.include?(composite)
|
287
|
+
# Links-as-hubs have foreign keys over natural indexes; these must be deleted.
|
288
|
+
composite.all_foreign_key_as_source_composite.to_a.each(&:retract)
|
289
|
+
end
|
290
|
+
|
291
|
+
# Add the audit and identity fields for the satellite:
|
292
|
+
satellites.values.each do |satellite|
|
293
|
+
audit_satellite composite, satellite
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
# Add the audit and foreign key fields to a satellite for this composite
|
299
|
+
def audit_satellite composite, satellite
|
300
|
+
trace :datavault, "Adding parent key and load time to satellite #{satellite.mapping.name.inspect}" do
|
301
|
+
|
302
|
+
# Add a Surrogate foreign Key to the parent composite
|
303
|
+
fk_target = composite.primary_index.all_index_field.single
|
304
|
+
fk_field = fork_component_to_new_parent(satellite.mapping, fk_target.component)
|
305
|
+
|
306
|
+
# Add a load DateTime value
|
307
|
+
date_field = @constellation.ValidFrom(
|
308
|
+
:new,
|
309
|
+
parent: satellite.mapping,
|
310
|
+
name: "Load"+datestamp_type_name,
|
311
|
+
object_type: datestamp_type
|
312
|
+
)
|
313
|
+
|
314
|
+
# Add a natural key:
|
315
|
+
natural_index =
|
316
|
+
@constellation.Index(:new, composite: satellite, is_unique: true,
|
317
|
+
presence_constraint: nil, composite_as_natural_index: satellite, composite_as_primary_index: satellite)
|
318
|
+
@constellation.IndexField(access_path: natural_index, ordinal: 0, component: fk_field)
|
319
|
+
@constellation.IndexField(access_path: natural_index, ordinal: 1, component: date_field)
|
320
|
+
|
321
|
+
# REVISIT: re-ranking members without a preferred_identifier does not rank the PK fields in order.
|
322
|
+
satellite.mapping.re_rank
|
323
|
+
|
324
|
+
# Add a foreign key to the hub
|
325
|
+
fk = @constellation.ForeignKey(
|
326
|
+
:new,
|
327
|
+
source_composite: satellite,
|
328
|
+
composite: composite,
|
329
|
+
absorption: nil # REVISIT: This is a ForeignKey without its mandatory Absorption. That's gonna hurt
|
330
|
+
)
|
331
|
+
@constellation.ForeignKeyField(foreign_key: fk, ordinal: 0, component: fk_field)
|
332
|
+
# This should be filled in by complete_foreign_keys, but there is no Absorption
|
333
|
+
@constellation.IndexField(access_path: fk, ordinal: 0, component: fk_target.component)
|
334
|
+
|
335
|
+
satellite.classify_constraints
|
336
|
+
satellite.all_local_constraint.map(&:local_constraint).each(&:retract)
|
337
|
+
leaf_constraints = satellite.mapping.all_leaf.flat_map(&:all_leaf_constraint).map(&:leaf_constraint).each(&:retract)
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
def datestamp_type_name
|
342
|
+
@datestamp_type_name ||= begin
|
343
|
+
[true, '', 'true', 'yes', nil].include?(t = @option_datestamp) ? 'DateTime' : t
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
def datestamp_type
|
348
|
+
@datestamp_type ||= begin
|
349
|
+
vocabulary = @composition.all_composite.to_a[0].mapping.object_type.vocabulary
|
350
|
+
@constellation.ObjectType[[[vocabulary.name], datestamp_type_name]] or
|
351
|
+
@constellation.ValueType(
|
352
|
+
vocabulary: vocabulary,
|
353
|
+
name: datestamp_type_name,
|
354
|
+
concept: [:new, :implication_rule => "datestamp injection"]
|
355
|
+
)
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
# Decide what to call a new satellite that will adopt this component
|
360
|
+
def name_satellite component
|
361
|
+
satellite_name =
|
362
|
+
if component.is_a?(MM::Absorption)
|
363
|
+
pc = component.parent_role.base_role.uniqueness_constraint and
|
364
|
+
pc.concept.all_concept_annotation.map{|ca| ca.mapping_annotation =~ /^satellite *(.*)/ && $1}.compact.uniq[0]
|
365
|
+
# REVISIT: How do we name the satellite for an Indicator? Add a Concept Annotation on the fact type?
|
366
|
+
end
|
367
|
+
satellite_name = satellite_name.words.capcase if satellite_name
|
368
|
+
satellite_name ||= component.root.mapping.name
|
369
|
+
satellite_name = apply_name(@option_sat_name, satellite_name)
|
370
|
+
end
|
371
|
+
|
372
|
+
# Create a new satellite for the same object_type as this composite
|
373
|
+
def create_satellite name, composite
|
374
|
+
mapping = @constellation.Mapping(:new, name: name, object_type: composite.mapping.object_type)
|
375
|
+
@constellation.Composite(mapping, composition: @composition)
|
376
|
+
end
|
377
|
+
|
378
|
+
# This component is being moved to a new composite, so any indexes that it or its
|
379
|
+
# children contribute to, cannot now be used to search for the specified composite.
|
380
|
+
# A component being moved to a satellite or a hub cannot keep its indices.
|
381
|
+
def remove_indices component
|
382
|
+
component.all_index_field.map(&:access_path).uniq.each(&:retract)
|
383
|
+
component.all_member.each{|member| remove_indices member}
|
384
|
+
end
|
385
|
+
|
386
|
+
def change_all_fk_source component, source_composite
|
387
|
+
if component.is_a?(MM::Absorption) && component.foreign_key
|
388
|
+
trace :datavault, "Setting new source composite for #{component.foreign_key.inspect}"
|
389
|
+
component.foreign_key.source_composite = source_composite
|
390
|
+
end
|
391
|
+
|
392
|
+
component.all_member.each do |member|
|
393
|
+
change_all_fk_source member, source_composite
|
394
|
+
end
|
395
|
+
end
|
396
|
+
|
397
|
+
# Move this member from its current parent to the satellite
|
398
|
+
def devolve_member_to_satellite satellite, member
|
399
|
+
remove_indices member
|
400
|
+
|
401
|
+
member.parent = satellite.mapping
|
402
|
+
change_all_fk_source member, satellite
|
403
|
+
trace :datavault, "Satellite #{satellite.mapping.name.inspect} field #{member.inspect}"
|
404
|
+
end
|
405
|
+
|
406
|
+
# This absorption reflects a time-varying fact type that involves another Hub, which becomes a new link.
|
407
|
+
# REVISIT: "make_copy" says that the original field must remain, because it's in its parent's natural key.
|
408
|
+
def devolve_absorption_to_link absorption, make_copy
|
409
|
+
trace :datavault, "Promote #{absorption.inspect} to a new Link" do
|
410
|
+
|
411
|
+
# REVISIT: Here we need a new objectified fact type with the same two players and the same readings,
|
412
|
+
# complete with LinkFactTypes. Then we need two Absorptions, one for each LinkFactType, and with
|
413
|
+
# the same child role names as the role names in our original fact type.
|
414
|
+
#
|
415
|
+
# The current code tries to re-use the same fact type, but the absorptions cannot work for both as
|
416
|
+
# the parent object type can only be one of the two types. That's why this is currently failing its
|
417
|
+
# validation tests.
|
418
|
+
|
419
|
+
link_name =
|
420
|
+
absorption.
|
421
|
+
parent_role.
|
422
|
+
fact_type.
|
423
|
+
reading_preferably_starting_with_role(absorption.parent_role).
|
424
|
+
expand([], false).words.capwords*' '
|
425
|
+
# A simpler naming, not using the fact type reading
|
426
|
+
# link_name = absorption.root.mapping.name + ' ' + absorption.child_role.name
|
427
|
+
|
428
|
+
link_from = absorption.parent.composite
|
429
|
+
link_to = absorption.foreign_key.composite
|
430
|
+
|
431
|
+
# A new composition that maps the same object type as this absorption's parent:
|
432
|
+
mapping = @constellation.Mapping(:new, name: link_name, object_type: absorption.parent_role.object_type)
|
433
|
+
link = @constellation.Composite(mapping, composition: @composition)
|
434
|
+
|
435
|
+
unless make_copy
|
436
|
+
remove_indices absorption
|
437
|
+
|
438
|
+
# Move the absorption across to here
|
439
|
+
absorption.parent = mapping
|
440
|
+
|
441
|
+
if absorption.foreign_key
|
442
|
+
trace :datavault, "Setting new source composite for #{absorption.foreign_key.inspect}"
|
443
|
+
absorption.foreign_key.source_composite = link
|
444
|
+
debugger unless absorption.foreign_key.all_foreign_key_field.single
|
445
|
+
fk2_component = absorption.foreign_key.all_foreign_key_field.single.component
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
# Add a surrogate key:
|
450
|
+
inject_surrogate link
|
451
|
+
|
452
|
+
# Add a Surrogate foreign Key to the link_from composite
|
453
|
+
fk1_target = link_from.primary_index.all_index_field.single
|
454
|
+
raise "Internal error: #{link_from.inspect} should have a surrogate key" unless fk1_target
|
455
|
+
# Here, we're jumping directly to the foreign key field.
|
456
|
+
# Normally we'd have the Absorption of the object type, containing the FK field.
|
457
|
+
# We have no fact type for this absorption; it should be the LinkFactType of the notional objectification
|
458
|
+
# This affects the absorption path comment on the related SQL coliumn, for example.
|
459
|
+
# REVISIT: Add the LinkFactType for the notional objectification, and use that.
|
460
|
+
fk1_component = fork_component_to_new_parent(mapping, fk1_target.component)
|
461
|
+
|
462
|
+
fk2_target = link_to.primary_index.all_index_field.single
|
463
|
+
if make_copy
|
464
|
+
# See the above comment for fk1_component; it aplies here also
|
465
|
+
fk2_component = fork_component_to_new_parent(mapping, fk2_target.component)
|
466
|
+
else
|
467
|
+
# We're using the leaf component of the absorption we moved across
|
468
|
+
end
|
469
|
+
|
470
|
+
# Add a natural key:
|
471
|
+
natural_index =
|
472
|
+
@constellation.Index(:new, composite: link, is_unique: true,
|
473
|
+
presence_constraint: nil, composite_as_natural_index: link)
|
474
|
+
@constellation.IndexField(access_path: natural_index, ordinal: 0, component: fk1_component)
|
475
|
+
@constellation.IndexField(access_path: natural_index, ordinal: 1, component: fk2_component)
|
476
|
+
|
477
|
+
# Add ForeignKeys
|
478
|
+
fk1 = @constellation.ForeignKey(
|
479
|
+
:new,
|
480
|
+
source_composite: link,
|
481
|
+
composite: link_from,
|
482
|
+
absorption: nil # REVISIT: This is a ForeignKey without its mandatory Absorption. That's gonna hurt
|
483
|
+
)
|
484
|
+
@constellation.ForeignKeyField(foreign_key: fk1, ordinal: 0, component: fk1_component)
|
485
|
+
# REVISIT: This should be filled in by complete_foreign_keys, but it has no Absorption
|
486
|
+
@constellation.IndexField(access_path: fk1, ordinal: 0, component: fk1_target.component)
|
487
|
+
|
488
|
+
if make_copy
|
489
|
+
fk2 = @constellation.ForeignKey(
|
490
|
+
:new,
|
491
|
+
source_composite: link,
|
492
|
+
composite: link_to,
|
493
|
+
absorption: nil # REVISIT: This is a ForeignKey without its mandatory Absorption. That's gonna hurt
|
494
|
+
)
|
495
|
+
@constellation.ForeignKeyField(foreign_key: fk2, ordinal: 0, component: fk2_component)
|
496
|
+
# REVISIT: This should be filled in by complete_foreign_keys, but it has no Absorption
|
497
|
+
@constellation.IndexField(access_path: fk2, ordinal: 0, component: fk2_target.component)
|
498
|
+
absorption.foreign_key.retract
|
499
|
+
end
|
500
|
+
|
501
|
+
=begin
|
502
|
+
issues = 0
|
503
|
+
link.validate do |object, problem|
|
504
|
+
$stderr.puts "#{object.inspect}: #{problem}"
|
505
|
+
issues += 1
|
506
|
+
end
|
507
|
+
debugger if issues > 0
|
508
|
+
=end
|
509
|
+
|
510
|
+
# Add a load DateTime value
|
511
|
+
date_field = @constellation.ValidFrom(:new,
|
512
|
+
parent: mapping,
|
513
|
+
name: "FirstLoad"+datestamp_type_name,
|
514
|
+
object_type: datestamp_type
|
515
|
+
)
|
516
|
+
mapping.re_rank
|
517
|
+
|
518
|
+
#link.mapping.re_rank
|
519
|
+
|
520
|
+
# devolve link, false
|
521
|
+
@link_composites << link
|
522
|
+
end
|
523
|
+
end
|
524
|
+
|
525
|
+
def apply_name pattern, name
|
526
|
+
pattern.sub(/\+/, name)
|
527
|
+
end
|
528
|
+
|
529
|
+
def rename_parents
|
530
|
+
@link_composites.each do |composite|
|
531
|
+
composite.mapping.name = apply_name(@option_link_name, composite.mapping.name)
|
532
|
+
end
|
533
|
+
@hub_composites.each do |composite|
|
534
|
+
composite.mapping.name = apply_name(@option_hub_name, composite.mapping.name)
|
535
|
+
end
|
536
|
+
@reference_composites.each do |composite|
|
537
|
+
composite.mapping.name = apply_name(@option_ref_name, composite.mapping.name)
|
538
|
+
end
|
539
|
+
end
|
540
|
+
|
541
|
+
end
|
542
|
+
|
543
|
+
publish_compositor(DataVault)
|
544
|
+
end
|
545
|
+
end
|