activefacts-compositions 1.9.6 → 1.9.8
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/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
|