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
@@ -1,7 +1,7 @@
|
|
1
1
|
#
|
2
2
|
# ActiveFacts Compositions, Metamodel aspect to build compacted column names for (leaf) Components
|
3
3
|
#
|
4
|
-
#
|
4
|
+
# Compresses the names arising from absorption paths into usable column names
|
5
5
|
#
|
6
6
|
# Copyright (c) 2016 Clifford Heath. Read the LICENSE file.
|
7
7
|
#
|
@@ -11,69 +11,69 @@ module ActiveFacts
|
|
11
11
|
module Metamodel
|
12
12
|
class Component
|
13
13
|
def column_name
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
14
|
+
column_path = path[1..-1]
|
15
|
+
prev_words = []
|
16
|
+
String::Words.new(
|
17
|
+
column_path.
|
18
|
+
inject([]) do |na, member|
|
19
|
+
is_absorption = member.is_a?(Absorption)
|
20
|
+
is_type_inheritance = is_absorption && member.parent_role.fact_type.is_a?(TypeInheritance)
|
21
|
+
fact_type = is_absorption && member.parent_role.fact_type
|
22
22
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
23
|
+
# If the parent object identifies the child via this absorption, skip it.
|
24
|
+
if member != column_path.first and
|
25
|
+
is_absorption and
|
26
|
+
!is_type_inheritance and
|
27
|
+
member.parent_role.base_role.is_identifying
|
28
|
+
trace :names, "Skipping #{member}, identifies non-initial object"
|
29
|
+
next na
|
30
|
+
end
|
31
31
|
|
32
|
-
|
32
|
+
words = member.name.words
|
33
33
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
34
|
+
if na.size > 0 && is_type_inheritance
|
35
|
+
# When traversing type inheritances, keep the subtype name, not the supertype names as well:
|
36
|
+
if member.child_role != fact_type.subtype_role
|
37
|
+
trace :names, "Skipping supertype #{member}"
|
38
|
+
next na
|
39
|
+
end
|
40
|
+
trace :names, "Eliding supertype in #{member}"
|
41
|
+
prev_words.size.times{na.pop}
|
42
42
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
43
|
+
elsif member.parent && member != column_path.first && is_absorption && member.child_role.base_role.is_identifying
|
44
|
+
# When Xyz is followed by identifying XyzID (even if we skipped the Xyz), truncate that to just ID
|
45
|
+
pnames = member.parent.name.words
|
46
|
+
if pnames == words[0, pnames.size]
|
47
|
+
pnames.size.times do
|
48
|
+
pnames.shift
|
49
|
+
words.shift
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
53
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
54
|
+
# If the reference is to the single identifying role of the object_type making the reference,
|
55
|
+
# strip the object_type name from the start of the reference role
|
56
|
+
if na.size > 0 and
|
57
|
+
is_absorption and
|
58
|
+
member.child_role.base_role.is_identifying and
|
59
|
+
(et = member.object_type).is_a?(EntityType) and
|
60
|
+
et.preferred_identifier.role_sequence.all_role_ref.size == 0 and
|
61
|
+
et.name.downcase == words[0][0...et.name.size].downcase
|
62
|
+
trace :columns, "truncating transitive identifying role #{words.inspect}"
|
63
|
+
words[0] = words[0][et.name.size..-1]
|
64
|
+
words.shift if words[0] == ''
|
65
|
+
end
|
66
66
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
67
|
+
prev_words = words
|
68
|
+
na += words.to_a
|
69
|
+
end.elide_repeated_subsequences do |a, b|
|
70
|
+
if a.is_a?(Array)
|
71
|
+
a.map{|e| e.downcase} == b.map{|e| e.downcase}
|
72
|
+
else
|
73
|
+
a.downcase == b.downcase
|
74
|
+
end
|
75
|
+
end
|
76
|
+
)
|
77
77
|
end
|
78
78
|
end
|
79
79
|
end
|
@@ -1,10 +1,7 @@
|
|
1
1
|
#
|
2
2
|
# ActiveFacts Compositions, Relational Compositor.
|
3
3
|
#
|
4
|
-
#
|
5
|
-
#
|
6
|
-
# Options to the constructor:
|
7
|
-
# single_sequence: The database technology can only increment one sequence per table (MS-SQL)
|
4
|
+
# Computes an Optimal Normal Form (close to 5NF) relational schema.
|
8
5
|
#
|
9
6
|
# Copyright (c) 2015 Clifford Heath. Read the LICENSE file.
|
10
7
|
#
|
@@ -13,789 +10,901 @@ require "activefacts/compositions"
|
|
13
10
|
module ActiveFacts
|
14
11
|
module Compositions
|
15
12
|
class Relational < Compositor
|
16
|
-
|
13
|
+
MM = ActiveFacts::Metamodel unless const_defined?(:MM)
|
14
|
+
|
15
|
+
def self.options
|
16
|
+
{
|
17
|
+
surrogates: ['Boolean', "Inject a surrogate key into each table whose primary key is not already suitable as a foreign key"]
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
17
21
|
def initialize constellation, name, options = {}
|
18
|
-
|
19
|
-
|
20
|
-
|
22
|
+
# Extract recognised options:
|
23
|
+
@option_surrogates = options.delete('surrogates')
|
24
|
+
super constellation, name, options
|
21
25
|
end
|
22
26
|
|
23
27
|
def generate
|
24
|
-
|
28
|
+
super
|
29
|
+
|
30
|
+
trace :relational_details!, "Generating relational composition" do
|
31
|
+
# Make a data structure to help in computing the tables
|
32
|
+
make_candidates
|
25
33
|
|
26
|
-
|
27
|
-
|
28
|
-
make_candidates
|
34
|
+
# Apply any obvious table/non-table factors
|
35
|
+
assign_default_tabulation
|
29
36
|
|
30
|
-
|
31
|
-
|
37
|
+
# Figure out how best to absorb things to reduce the number of tables
|
38
|
+
optimise_absorption
|
32
39
|
|
33
|
-
|
34
|
-
|
40
|
+
# Actually make a Composite object for each table:
|
41
|
+
make_composites
|
35
42
|
|
36
|
-
|
37
|
-
|
43
|
+
# If a value type has been mapped to a table, add a column to hold its value
|
44
|
+
inject_value_fields
|
38
45
|
|
39
|
-
|
40
|
-
|
46
|
+
# Inject surrogate keys if the options ask for that
|
47
|
+
inject_surrogates if @option_surrogates
|
41
48
|
|
42
|
-
|
43
|
-
|
49
|
+
# Remove the un-used absorption paths
|
50
|
+
delete_reverse_absorptions
|
44
51
|
|
45
|
-
|
46
|
-
|
52
|
+
# Traverse the absorbed objects to build the path to each required column, including foreign keys:
|
53
|
+
absorb_all_columns
|
47
54
|
|
48
|
-
|
49
|
-
absorb_all_columns
|
55
|
+
devolve_all
|
50
56
|
|
51
|
-
|
52
|
-
|
57
|
+
# Populate the target fields of foreign keys
|
58
|
+
complete_foreign_keys
|
53
59
|
|
54
|
-
|
55
|
-
|
56
|
-
|
60
|
+
# Remove mappings for objects we have absorbed
|
61
|
+
clean_unused_mappings
|
62
|
+
end
|
57
63
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
64
|
+
trace :relational!, "Full #{self.class.basename} composition" do
|
65
|
+
@composition.all_composite.sort_by{|composite| composite.mapping.name}.each do |composite|
|
66
|
+
composite.show_trace
|
67
|
+
end
|
68
|
+
end
|
63
69
|
end
|
64
70
|
|
65
71
|
def make_candidates
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
72
|
+
@candidates = @binary_mappings.inject({}) do |hash, (absorption, mapping)|
|
73
|
+
hash[mapping.object_type] = Candidate.new(self, mapping)
|
74
|
+
hash
|
75
|
+
end
|
70
76
|
end
|
71
77
|
|
72
78
|
def assign_default_tabulation
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
79
|
+
trace :relational_defaults!, "Preparing relational composition by setting default assumptions" do
|
80
|
+
@candidates.each do |object_type, candidate|
|
81
|
+
candidate.assign_default(@composition)
|
82
|
+
end
|
83
|
+
end
|
78
84
|
end
|
79
85
|
|
80
86
|
def optimise_absorption
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
87
|
+
trace :relational_optimiser!, "Optimise Relational Composition" do
|
88
|
+
undecided = @candidates.keys.select{|object_type| @candidates[object_type].is_tentative}
|
89
|
+
pass = 0
|
90
|
+
finalised = []
|
91
|
+
begin
|
92
|
+
pass += 1
|
93
|
+
trace :relational_optimiser, "Starting optimisation pass #{pass}" do
|
94
|
+
finalised = optimise_absorption_pass(undecided)
|
95
|
+
end
|
96
|
+
trace :relational_optimiser, "Finalised #{finalised.size} on this pass: #{finalised.map{|f| f.name}*', '}"
|
97
|
+
undecided -= finalised
|
98
|
+
end while !finalised.empty?
|
99
|
+
end
|
94
100
|
end
|
95
101
|
|
96
102
|
def optimise_absorption_pass undecided
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
103
|
+
undecided.select do |object_type|
|
104
|
+
candidate = @candidates[object_type]
|
105
|
+
trace :relational_optimiser, "Considering possible status of #{object_type.name}" do
|
106
|
+
|
107
|
+
# Rule 1: Always absorb an objectified unary into its role player (unless its forced to be separate)
|
108
|
+
if !object_type.is_separate && (f = object_type.fact_type) && f.all_role.size == 1
|
109
|
+
absorbing_ref = candidate.mapping.all_member.detect{|a| a.is_a?(MM::Absorption) and a.child_role.base_role == f.all_role.single}
|
110
|
+
raise "REVISIT: Internal error" unless absorbing_ref.parent_role.object_type == object_type
|
111
|
+
absorbing_ref = absorbing_ref.flip!
|
112
|
+
candidate.full_absorption =
|
113
|
+
@constellation.FullAbsorption(composition: @composition, absorption: absorbing_ref, object_type: object_type)
|
114
|
+
trace :relational_optimiser, "Fully absorb objectified unary #{object_type.name} into #{f.all_role.single.object_type.name}"
|
115
|
+
candidate.definitely_not_table
|
116
|
+
next object_type
|
117
|
+
end
|
118
|
+
|
119
|
+
# Rule 2: If the preferred_identifier contains one role only, played by an entity type that can absorb us, do that:
|
120
|
+
# (Leave pi_roles intact for further use below)
|
121
|
+
absorbing_ref = nil
|
122
|
+
pi_roles = []
|
123
|
+
if object_type.is_a?(MM::EntityType) and # We're an entity type
|
124
|
+
pi_roles = object_type.preferred_identifier_roles and # Our PI
|
125
|
+
pi_roles.size == 1 and # has one role
|
126
|
+
single_pi_role = pi_roles[0] and # that role is
|
127
|
+
single_pi_role.object_type.is_a?(MM::EntityType) and # played by another Entity Type
|
128
|
+
absorbing_ref =
|
129
|
+
candidate.mapping.all_member.detect do |absorption|
|
130
|
+
absorption.is_a?(MM::Absorption) && absorption.child_role.base_role == single_pi_role
|
131
|
+
end
|
132
|
+
|
133
|
+
absorbing_ref = absorbing_ref.forward_absorption || absorbing_ref.flip!
|
134
|
+
candidate.full_absorption =
|
135
|
+
@constellation.FullAbsorption(composition: @composition, absorption: absorbing_ref, object_type: object_type)
|
136
|
+
trace :relational_optimiser, "EntityType #{single_pi_role.object_type.name} identifies EntityType #{object_type.name}, so fully absorbs it via #{absorbing_ref.inspect}"
|
137
|
+
candidate.definitely_not_table
|
138
|
+
next object_type
|
139
|
+
end
|
140
|
+
|
141
|
+
# Rule 3: If there's more than one absorption path and any functional dependencies that can't absorb us, it's a table
|
142
|
+
non_identifying_refs_from =
|
143
|
+
candidate.references_from.reject do |member|
|
144
|
+
case member
|
145
|
+
when MM::Absorption
|
146
|
+
pi_roles.include?(member.child_role.base_role)
|
147
|
+
when MM::Indicator
|
148
|
+
pi_roles.include?(member.role)
|
149
|
+
else
|
150
|
+
false
|
151
|
+
end
|
152
|
+
end
|
153
|
+
trace :relational_optimiser, "#{object_type.name} has #{non_identifying_refs_from.size} non-identifying functional roles" do
|
154
|
+
non_identifying_refs_from.each do |a|
|
155
|
+
trace :relational_optimiser, a.inspect
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
trace :relational_optimiser, "#{object_type.name} has #{candidate.references_to.size} references to it" do
|
160
|
+
candidate.references_to.each do |a|
|
161
|
+
trace :relational_optimiser, a.inspect
|
162
|
+
end
|
163
|
+
end
|
164
|
+
if candidate.references_to.size > 1 and # More than one place wants us
|
165
|
+
non_identifying_refs_from.size > 0 # And we carry dependent values so cannot be absorbed
|
166
|
+
trace :relational_optimiser, "#{object_type.name} has #{non_identifying_refs_from.size} non-identifying functional dependencies and #{candidate.references_to.size} absorption paths so 3NF requires it be a table"
|
167
|
+
candidate.definitely_table
|
168
|
+
next object_type
|
169
|
+
end
|
170
|
+
|
171
|
+
# At this point, this object either has no functional dependencies or only one place it would be absorbed
|
172
|
+
next false if !candidate.is_table # We can't reduce the number of tables by absorbing this one
|
173
|
+
|
174
|
+
absorption_paths =
|
175
|
+
( non_identifying_refs_from + # But we should exclude any that are already involved in an absorption; pre-decided ET=>ET or supertype absorption!
|
176
|
+
candidate.references_to # These are our reverse absorptions that could absorb us
|
177
|
+
).select do |a|
|
178
|
+
next false unless a.is_a?(MM::Absorption) # Skip Indicators, we can't be absorbed there
|
179
|
+
child_candidate = @candidates[a.child_role.object_type]
|
180
|
+
|
181
|
+
# It's ok if we absorbed them already
|
182
|
+
next true if a.full_absorption && child_candidate.full_absorption.absorption != a
|
183
|
+
|
184
|
+
# If our counterpart is a full absorption, don't try to reverse that!
|
185
|
+
next false if (a.forward_absorption || a.reverse_absorption).full_absorption
|
186
|
+
|
187
|
+
# Otherwise the other end must already be a table or fully absorbed into one
|
188
|
+
next false unless child_candidate.is_table || child_candidate.full_absorption
|
189
|
+
|
190
|
+
next false unless a.child_role.is_unique && a.parent_role.is_unique # Must be one-to-one
|
191
|
+
|
192
|
+
# next true if pi_roles.size == 1 && pi_roles.include?(a.parent_role) # Allow the sole identifying role for this object
|
193
|
+
next false unless a.parent_role.is_mandatory # Don't absorb an object along a non-mandatory role
|
194
|
+
true
|
195
|
+
end
|
196
|
+
|
197
|
+
trace :relational_optimiser, "#{object_type.name} has #{absorption_paths.size} absorption paths"
|
198
|
+
|
199
|
+
# Rule 4: If this object can be fully absorbed along non-identifying roles, do that (maybe flip some absorptions)
|
200
|
+
if absorption_paths.size > 0
|
201
|
+
trace :relational_optimiser, "#{object_type.name} is fully absorbed in #{absorption_paths.size} places" do
|
202
|
+
absorption_paths.each do |a|
|
203
|
+
a = a.flip! if a.forward_absorption
|
204
|
+
trace :relational_optimiser, "#{object_type.name} is fully absorbed via #{a.inspect}"
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
candidate.definitely_not_table
|
209
|
+
next object_type
|
210
|
+
end
|
211
|
+
|
212
|
+
# Rule 5: If this object has no functional dependencies (only its identifier), it can be absorbed in multiple places
|
213
|
+
# We don't create FullAbsorptions, because they're only used to resolve references to this object; and there are none here
|
214
|
+
refs_to = candidate.references_to.reject{|a|a.parent_role.base_role.is_identifying}
|
215
|
+
if !refs_to.empty? and non_identifying_refs_from.size == 0
|
216
|
+
refs_to.map! do |a|
|
217
|
+
a = a.flip! if a.reverse_absorption # We were forward, but the other end must be
|
218
|
+
a.forward_absorption
|
219
|
+
end
|
220
|
+
trace :relational_optimiser, "#{object_type.name} is fully absorbed in #{refs_to.size} places: #{refs_to.map{|ref| ref.inspect}*", "}"
|
221
|
+
candidate.definitely_not_table
|
222
|
+
next object_type
|
223
|
+
end
|
224
|
+
|
225
|
+
false # Otherwise we failed to make a decision about this object type
|
226
|
+
end
|
227
|
+
end
|
222
228
|
end
|
223
229
|
|
224
230
|
# Remove the unused reverse absorptions:
|
225
231
|
def delete_reverse_absorptions
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
232
|
+
@binary_mappings.each do |object_type, mapping|
|
233
|
+
mapping.all_member.to_a. # Avoid problems with deletion from all_member
|
234
|
+
each do |member|
|
235
|
+
next unless member.is_a?(MM::Absorption)
|
236
|
+
member.retract if member.forward_absorption # This is the reverse of some absorption
|
237
|
+
end
|
238
|
+
mapping.re_rank
|
239
|
+
end
|
234
240
|
end
|
235
241
|
|
236
242
|
# After all table/non-table decisions are made, convert Mappings for tables into Composites and retract the rest:
|
237
243
|
def make_composites
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
244
|
+
@composites = {}
|
245
|
+
@candidates.keys.to_a.each do |object_type|
|
246
|
+
candidate = @candidates[object_type]
|
247
|
+
|
248
|
+
if candidate.is_table
|
249
|
+
make_composite candidate
|
250
|
+
else
|
251
|
+
@candidates.delete(object_type)
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
242
255
|
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
@candidates.delete(object_type)
|
248
|
-
end
|
249
|
-
end
|
256
|
+
def make_composite candidate
|
257
|
+
mapping = candidate.mapping
|
258
|
+
@composites[mapping.object_type] =
|
259
|
+
@constellation.Composite(mapping, composition: @composition)
|
250
260
|
end
|
251
261
|
|
252
262
|
# Inject a ValueField for each value type that's a table:
|
253
263
|
def inject_value_fields
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
264
|
+
@composition.all_composite.each do |composite|
|
265
|
+
mapping = composite.mapping
|
266
|
+
if mapping.object_type.is_a?(MM::ValueType) and # Composite needs a ValueField
|
267
|
+
!mapping.all_member.detect{|m| m.is_a?(MM::ValueField)} # And don't already have one
|
268
|
+
trace :relational_columns, "Adding value field for #{mapping.object_type.name}"
|
269
|
+
@constellation.ValueField(
|
270
|
+
:new,
|
271
|
+
parent: mapping,
|
272
|
+
name: mapping.object_type.name+" Value",
|
273
|
+
object_type: mapping.object_type
|
274
|
+
)
|
275
|
+
mapping.re_rank
|
276
|
+
end
|
277
|
+
end
|
268
278
|
end
|
269
279
|
|
270
280
|
def inject_surrogates
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
281
|
+
composites = @composition.all_composite.to_a
|
282
|
+
return if composites.empty?
|
283
|
+
|
284
|
+
trace :surrogates, "Injecting any required surrogates" do
|
285
|
+
@composition.all_composite.each do |composite|
|
286
|
+
next unless needs_surrogate(composite)
|
287
|
+
inject_surrogate composite
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
def surrogate_type
|
293
|
+
@surrogate_type ||= begin
|
294
|
+
surrogate_type_name = [true, '', 'true', 'yes', nil].include?(t = @option_surrogates) ? 'Auto Counter' : t
|
295
|
+
# REVISIT: Crappy: choose the first (currently always single)
|
296
|
+
vocabulary = @composition.all_composite.to_a[0].mapping.object_type.vocabulary
|
297
|
+
@constellation.ValueType(
|
298
|
+
vocabulary: vocabulary,
|
299
|
+
name: surrogate_type_name,
|
300
|
+
concept: [:new, :implication_rule => "surrogate injection"]
|
301
|
+
)
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
def inject_surrogate composite, extension = ' ID'
|
306
|
+
trace :surrogates, "Injecting surrogate for #{composite.inspect}" do
|
307
|
+
surrogate_component =
|
308
|
+
@constellation.SurrogateKey(
|
309
|
+
:new,
|
310
|
+
parent: composite.mapping,
|
311
|
+
name: composite.mapping.name+extension,
|
312
|
+
object_type: surrogate_type
|
313
|
+
)
|
314
|
+
index =
|
315
|
+
@constellation.Index(:new, composite: composite, is_unique: true,
|
316
|
+
presence_constraint: nil, composite_as_primary_index: composite)
|
317
|
+
@constellation.IndexField(access_path: index, ordinal: 0, component: surrogate_component)
|
318
|
+
composite.mapping.re_rank
|
319
|
+
surrogate_component
|
320
|
+
end
|
301
321
|
end
|
302
322
|
|
303
323
|
def needs_surrogate(composite)
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
324
|
+
object_type = composite.mapping.object_type
|
325
|
+
if MM::ValueType === object_type
|
326
|
+
trace :surrogates, "#{composite.inspect} is a ValueType that #{object_type.is_auto_assigned ? "is auto-assigned already" : "requires a surrogate" }"
|
327
|
+
return !object_type.is_auto_assigned
|
328
|
+
end
|
329
|
+
|
330
|
+
non_key_members, key_members = composite.mapping.all_member.reject do |member|
|
331
|
+
member.is_a?(MM::Absorption) and member.forward_absorption
|
332
|
+
end.partition do |member|
|
333
|
+
member.rank_key[0] > MM::Component::RANK_IDENT
|
334
|
+
end
|
335
|
+
|
336
|
+
non_fk_surrogate =
|
337
|
+
key_members.detect do |member|
|
338
|
+
next true unless member.is_a?(MM::Absorption)
|
339
|
+
next false if @composites[member.object_type] or @composition.all_full_absorption[member.object_type] # It's a table or absorbed into one
|
340
|
+
true
|
341
|
+
end
|
342
|
+
|
343
|
+
if key_members.size > 1
|
344
|
+
# Multi-part identifiers are only allowed if:
|
345
|
+
# * each part is a foreign key (i.e. it's a join table),
|
346
|
+
# * there are no other columns (that might require updating) and
|
347
|
+
# * the object is not the target of a foreign key:
|
348
|
+
if non_fk_surrogate
|
349
|
+
trace :surrogates, "#{composite.inspect} has non-FK identifiers so requires a surrogate"
|
350
|
+
return true
|
351
|
+
end
|
352
|
+
|
353
|
+
if non_key_members.size > 0
|
354
|
+
trace :surrogates, "#{composite.inspect} has non-identifying fields so requires a surrogate"
|
355
|
+
return true
|
356
|
+
end
|
357
|
+
|
358
|
+
if @candidates[object_type].references_to.size > 0
|
359
|
+
trace :surrogates, "#{composite.inspect} is the target of at least one foreign key so requires a surrogate"
|
360
|
+
return true
|
361
|
+
end
|
362
|
+
|
363
|
+
trace :surrogates, "#{composite.inspect} is a join table that does NOT require a surrogate"
|
364
|
+
return false
|
365
|
+
end
|
366
|
+
|
367
|
+
# A single-part PK is replaced by a surrogate unless the single part is a surrogate, an FK to a surrogate, or is an Absorbed auto-assigned VT
|
368
|
+
|
369
|
+
key_member = key_members[0]
|
370
|
+
if !non_fk_surrogate
|
371
|
+
trace :surrogates, "#{composite.inspect} has an identifier that's an FK so does NOT require a surrogate"
|
372
|
+
return false
|
373
|
+
end
|
374
|
+
|
375
|
+
if key_member.is_a?(MM::SurrogateKey)
|
376
|
+
trace :surrogates, "#{composite.inspect} already has an injected SurrogateKey so does NOT require a surrogate"
|
377
|
+
return false
|
378
|
+
end
|
379
|
+
unless key_member.is_a?(MM::Absorption)
|
380
|
+
trace :surrogates, "#{composite.inspect} is identified by a non-Absorption so requires a surrogate"
|
381
|
+
return true
|
382
|
+
end
|
383
|
+
if key_member.object_type.is_a?(MM::EntityType)
|
384
|
+
trace :surrogates, "#{composite.inspect} is identified by another entity type so requires a surrogate"
|
385
|
+
return true
|
386
|
+
end
|
387
|
+
if key_member.object_type.is_auto_assigned
|
388
|
+
trace :surrogates, "#{composite.inspect} already has an auto-assigned key so does NOT require a surrogate"
|
389
|
+
return false
|
390
|
+
end
|
391
|
+
trace :surrogates, "#{composite.inspect} requires a surrogate"
|
392
|
+
return true
|
373
393
|
end
|
374
394
|
|
375
395
|
def clean_unused_mappings
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
396
|
+
@candidates.keys.to_a.each do |object_type|
|
397
|
+
candidate = @candidates[object_type]
|
398
|
+
next if candidate.is_table
|
399
|
+
mapping = candidate.mapping
|
400
|
+
mapping.retract
|
401
|
+
@binary_mappings.delete(object_type)
|
402
|
+
end
|
403
|
+
end
|
404
|
+
|
405
|
+
def is_empty_inheritance mapping
|
406
|
+
# Cannot be an empty inheritance unless it's an TypeInheritance absorption
|
407
|
+
return false if !mapping.is_a?(MM::Absorption) || !mapping.parent_role.fact_type.is_a?(MM::TypeInheritance)
|
408
|
+
|
409
|
+
# It's empty if it's a TypeInheritance which has no non-empty members
|
410
|
+
!mapping.all_member.to_a.any? do |member|
|
411
|
+
!is_empty_inheritance(member)
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
def elide_empty_inheritance mapping
|
416
|
+
mapping.all_member.to_a.each do |member|
|
417
|
+
if member.is_a?(MM::Absorption) && member.parent_role.fact_type.is_a?(MM::TypeInheritance)
|
418
|
+
elide_empty_inheritance member
|
419
|
+
if member.all_member.size == 0
|
420
|
+
trace :relational, "Retracting empty inheritance #{member.inspect}"
|
421
|
+
member.retract
|
422
|
+
end
|
423
|
+
end
|
424
|
+
end
|
383
425
|
end
|
384
426
|
|
385
427
|
# Absorb all items which aren't tables (and keys to those which are) recursively
|
386
428
|
def absorb_all_columns
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
429
|
+
trace :relational_columns!, "Computing contents of all tables" do
|
430
|
+
@composition.all_composite_by_name.each do |composite|
|
431
|
+
trace :relational_columns, "Computing contents of #{composite.mapping.name}" do
|
432
|
+
absorb_all composite.mapping, composite.mapping
|
433
|
+
end
|
434
|
+
end
|
435
|
+
end
|
436
|
+
end
|
437
|
+
|
438
|
+
# This method duplicates part of the absorb_all process,
|
439
|
+
# looking for foreign keys from this composite.
|
440
|
+
# This must be done in a Data Vault mapping before we decide
|
441
|
+
# what will be a hub and what will be a link, and that controls
|
442
|
+
# whether we will absorb a foreign key or a copy of the natural
|
443
|
+
# key while expanding a table's identifiers.
|
444
|
+
# Hub tables never absorb a surrogate FK, only natural keys.
|
445
|
+
def enumerate_foreign_keys mapping, from = nil, accumulator = [], path = []
|
446
|
+
return if path.include?(mapping)
|
447
|
+
path << mapping
|
448
|
+
from ||= mapping
|
449
|
+
|
450
|
+
# REVISIT: This corrects some instability (should not actually be order-dependent) but doesn't fix the underlying problem
|
451
|
+
mapping.re_rank
|
452
|
+
ordered = from.all_member.sort_by(&:ordinal)
|
453
|
+
|
454
|
+
ordered.each do |member|
|
455
|
+
# Only consider forward Absorptions:
|
456
|
+
next if !member.is_a?(MM::Absorption)
|
457
|
+
next if member.forward_absorption
|
458
|
+
|
459
|
+
child_object_type = member.child_role.object_type
|
460
|
+
child_mapping = @binary_mappings[child_object_type]
|
461
|
+
if child_mapping.composite
|
462
|
+
trace :fks, "FK to #{member.child_role.name} in #{member.inspect_reading}"
|
463
|
+
accumulator << child_mapping.composite
|
464
|
+
next
|
465
|
+
end
|
466
|
+
|
467
|
+
full_absorption = child_object_type.all_full_absorption[@composition]
|
468
|
+
if full_absorption && full_absorption.absorption.parent_role.fact_type != member.parent_role.fact_type
|
469
|
+
begin # Follow transitive target absorption
|
470
|
+
child_object_type = full_absorption.absorption.parent_role.object_type
|
471
|
+
end while full_absorption = child_object_type.all_full_absorption[@composition]
|
472
|
+
child_mapping = @binary_mappings[child_object_type]
|
473
|
+
trace :fks, "FK to #{child_mapping.name} in #{member.inspect_reading} (for fully-absorbed #{member.child_role.name})"
|
474
|
+
accumulator << child_mapping.composite
|
475
|
+
next
|
476
|
+
end
|
477
|
+
|
478
|
+
trace :fks, "Descending all of #{member.child_role.name} in #{member.inspect_reading}" do
|
479
|
+
enumerate_foreign_keys member, child_mapping, accumulator, path
|
480
|
+
end
|
481
|
+
end
|
482
|
+
accumulator
|
483
|
+
end
|
484
|
+
|
485
|
+
def devolve_all
|
486
|
+
# Data Vaults have satellites, not normal relational schemas.
|
394
487
|
end
|
395
488
|
|
396
489
|
# This member is an Absorption. Process it recursively, absorbing all its members or just a key
|
397
490
|
# depending on whether the absorbed object is a Composite (or absorbed into one) or not.
|
398
491
|
def absorb_nested mapping, member, paths
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
492
|
+
# Should we absorb a foreign key or the whole contents?
|
493
|
+
|
494
|
+
child_object_type = member.child_role.object_type
|
495
|
+
child_mapping = @binary_mappings[child_object_type]
|
496
|
+
if child_mapping.composite
|
497
|
+
trace :relational_columns?, "Absorbing FK to #{member.child_role.name} in #{member.inspect_reading}" do
|
498
|
+
paths[member] = @constellation.ForeignKey(:new, source_composite: mapping.root, composite: child_mapping.composite, absorption: member)
|
499
|
+
absorb_key member, child_mapping, paths
|
500
|
+
return
|
501
|
+
end
|
502
|
+
end
|
503
|
+
|
504
|
+
# Is our target object_type fully absorbed (and not through this absorption)?
|
505
|
+
full_absorption = child_object_type.all_full_absorption[@composition]
|
506
|
+
# We can't use member.full_absorption here, as it's not populated on forked copies
|
507
|
+
# if full_absorption && full_absorption != member.full_absorption
|
508
|
+
if full_absorption && full_absorption.absorption.parent_role.fact_type != member.parent_role.fact_type
|
509
|
+
|
510
|
+
# REVISIT: This should be done by recursing to absorb_key, not using a loop
|
511
|
+
absorption = member # Retain this for the ForeignKey
|
512
|
+
begin # Follow transitive target absorption
|
513
|
+
member = mirror(full_absorption.absorption, member)
|
514
|
+
child_object_type = full_absorption.absorption.parent_role.object_type
|
515
|
+
end while full_absorption = child_object_type.all_full_absorption[@composition]
|
516
|
+
child_mapping = @binary_mappings[child_object_type]
|
517
|
+
|
518
|
+
trace :relational_columns?, "Absorbing FK to #{absorption.child_role.name} (fully absorbed into #{child_object_type.name}) in #{member.inspect_reading}" do
|
519
|
+
paths[absorption] = @constellation.ForeignKey(:new, source_composite: mapping.root, composite: child_mapping.composite, absorption: absorption)
|
520
|
+
absorb_key member, child_mapping, paths
|
521
|
+
end
|
522
|
+
return
|
523
|
+
end
|
524
|
+
|
525
|
+
trace :relational_columns?, "Absorbing all of #{member.child_role.name} in #{member.inspect_reading}" do
|
526
|
+
absorb_all member, child_mapping, paths
|
527
|
+
end
|
528
|
+
end
|
529
|
+
|
530
|
+
# May be overridden in subclasses
|
531
|
+
def prefer_natural_key building_natural_key, source_composite, target_composite
|
532
|
+
false
|
436
533
|
end
|
437
534
|
|
438
535
|
# Recursively add members to this component for the existential roles of
|
439
536
|
# the composite mapping for the absorbed (child_role) object:
|
440
537
|
def absorb_key mapping, target, paths
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
538
|
+
building_natural_key = paths.detect{|k,i| i.is_a?(MM::Index) && i.composite_as_natural_index}
|
539
|
+
prefer_natural = prefer_natural_key(building_natural_key, mapping.root, target.composite)
|
540
|
+
prefer_natural = false unless !target.composite || target.composite.primary_index != target.composite.natural_index
|
541
|
+
target.re_rank
|
542
|
+
target.all_member.sort_by(&:ordinal).each do |member|
|
543
|
+
rank = member.rank_key[0]
|
544
|
+
next unless rank <= MM::Component::RANK_IDENT
|
545
|
+
if rank == MM::Component::RANK_SURROGATE && prefer_natural
|
546
|
+
next
|
547
|
+
end
|
548
|
+
member = fork_component_to_new_parent mapping, member
|
549
|
+
augment_paths paths, member
|
550
|
+
if rank == MM::Component::RANK_SURROGATE && !prefer_natural
|
551
|
+
break # Will always be first (higher rank), and usurps others
|
552
|
+
elsif member.is_a?(MM::Absorption)
|
553
|
+
object_type = member.child_role.object_type
|
554
|
+
full_absorption = @composition.all_full_absorption[member.child_role.object_type]
|
555
|
+
if full_absorption
|
556
|
+
# The target object is fully absorbed. Absorb a key to where it was absorbed
|
557
|
+
# We can't recurse here, because we must descend supertype absorptions
|
558
|
+
while full_absorption
|
559
|
+
trace :relational_columns?, "Absorbing key of fully absorbed #{member.child_role.name}" do
|
560
|
+
member = mirror full_absorption.absorption, member
|
561
|
+
augment_paths paths, member
|
562
|
+
# Descend so the key fields get fully populated
|
563
|
+
absorb_key member, full_absorption.absorption.parent, paths
|
564
|
+
full_absorption = @composition.all_full_absorption[member.child_role.object_type]
|
565
|
+
end
|
566
|
+
end
|
567
|
+
else
|
568
|
+
absorb_key member, @binary_mappings[member.child_role.object_type], paths
|
569
|
+
end
|
570
|
+
end
|
571
|
+
end
|
572
|
+
# mapping.re_rank
|
468
573
|
end
|
469
574
|
|
470
575
|
# Augment the mapping with copies of the children of the "from" mapping.
|
471
576
|
# At the top level, no "from" is given and the children already exist
|
472
577
|
def absorb_all mapping, from, paths = {}
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
# mapping.re_rank
|
578
|
+
top_level = mapping == from
|
579
|
+
|
580
|
+
pcs = []
|
581
|
+
newpaths = {}
|
582
|
+
if mapping.composite || mapping.full_absorption
|
583
|
+
pcs = find_uniqueness_constraints(mapping)
|
584
|
+
|
585
|
+
# Don't build an index from the same PresenceConstraint twice on the same composite (e.g. for a subtype)
|
586
|
+
existing_pcs = mapping.root.all_access_path.select{|ap| MM::Index === ap}.map(&:presence_constraint)
|
587
|
+
newpaths = make_new_paths mapping, paths.keys+existing_pcs, pcs
|
588
|
+
end
|
589
|
+
|
590
|
+
from.re_rank
|
591
|
+
ordered = from.all_member.sort_by(&:ordinal)
|
592
|
+
ordered.each do |member|
|
593
|
+
trace :relational_columns, proc {"#{top_level ? 'Existing' : 'Absorbing'} #{member.inspect}"} do
|
594
|
+
unless top_level # Top-level members are already instantiated
|
595
|
+
member = fork_component_to_new_parent(mapping, member)
|
596
|
+
end
|
597
|
+
rel = paths.merge(relevant_paths(newpaths, member))
|
598
|
+
augment_paths rel, member
|
599
|
+
|
600
|
+
if member.is_a?(MM::Absorption) && !member.forward_absorption
|
601
|
+
# Only forward absorptions here please...
|
602
|
+
absorb_nested mapping, member, rel
|
603
|
+
end
|
604
|
+
end
|
605
|
+
end
|
606
|
+
|
607
|
+
newpaths.values.select{|ix| ix.all_index_field.size == 0}.each(&:retract)
|
505
608
|
end
|
506
609
|
|
507
610
|
# Find all PresenceConstraints to index the object in this Mapping
|
508
611
|
def find_uniqueness_constraints mapping
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
612
|
+
return [] unless mapping.object_type.is_a?(MM::EntityType)
|
613
|
+
|
614
|
+
start_roles =
|
615
|
+
mapping.
|
616
|
+
object_type.
|
617
|
+
all_role_transitive. # Includes objectification roles for objectified fact types
|
618
|
+
select do |role|
|
619
|
+
(role.is_unique || # Must be unique on near role
|
620
|
+
role.fact_type.is_unary) && # Or be a unary role
|
621
|
+
!(role.fact_type.is_a?(MM::TypeInheritance) && role == role.fact_type.supertype_role) # allow roles as subtype
|
622
|
+
end.
|
623
|
+
map(&:counterpart). # (Same role if it's a unary)
|
624
|
+
compact. # Ignore nil counterpart of a role in an n-ary
|
625
|
+
map(&:base_role). # In case it's a link fact type
|
626
|
+
uniq
|
627
|
+
|
628
|
+
pcs =
|
629
|
+
start_roles.
|
630
|
+
flat_map(&:all_role_ref). # All role_refs
|
631
|
+
map(&:role_sequence). # The role_sequence
|
632
|
+
uniq.
|
633
|
+
flat_map(&:all_presence_constraint).
|
634
|
+
uniq.
|
635
|
+
reject do |pc|
|
636
|
+
pc.max_frequency != 1 || # Must be unique
|
637
|
+
pc.enforcement || # and alethic
|
638
|
+
pc.role_sequence.all_role_ref.detect do |rr|
|
639
|
+
!start_roles.include?(rr.role) # and span only valid roles
|
640
|
+
end || # and not be the full absorption path
|
641
|
+
( # Reject a constraint that caused full absorption
|
642
|
+
pc.role_sequence.all_role_ref.size == 1 and
|
643
|
+
mapping.is_a?(MM::Absorption) and
|
644
|
+
fa = mapping.full_absorption and
|
645
|
+
pc.role_sequence.all_role_ref.single.role.base_role == fa.absorption.parent_role.base_role
|
646
|
+
)
|
647
|
+
end # Alethic uniqueness constraint on far end
|
648
|
+
|
649
|
+
non_absorption_pcs = pcs.reject do |pc|
|
650
|
+
# An absorption PC is a PC that covers some role that is involved in a FullAbsorption
|
651
|
+
full_absorptions =
|
652
|
+
pc.
|
653
|
+
role_sequence.
|
654
|
+
all_role_ref.
|
655
|
+
map(&:role).
|
656
|
+
flat_map do |role|
|
657
|
+
(role.all_absorption_as_parent_role.to_a + role.all_absorption_as_child_role.to_a).
|
658
|
+
select do |abs|
|
659
|
+
abs.full_absorption && abs.full_absorption.composition == @composition
|
660
|
+
end
|
661
|
+
end
|
662
|
+
full_absorptions.size > 0
|
663
|
+
end
|
664
|
+
pcs = non_absorption_pcs
|
665
|
+
|
666
|
+
trace :relational_paths, "Uniqueness Constraints for #{mapping.object_type.name}" do
|
667
|
+
pcs.each do |pc|
|
668
|
+
trace :relational_paths, "#{pc.describe.inspect}#{pc.is_preferred_identifier ? ' (PI)' : ''}"
|
669
|
+
end
|
670
|
+
end
|
671
|
+
|
672
|
+
pcs
|
570
673
|
end
|
571
674
|
|
572
675
|
def make_new_paths mapping, existing_pcs, pcs
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
676
|
+
newpaths = {}
|
677
|
+
new_pcs = pcs-existing_pcs
|
678
|
+
trace :relational_paths?, "Adding #{new_pcs.size} new indices for presence constraints on #{mapping.inspect}" do
|
679
|
+
new_pcs.each do |pc|
|
680
|
+
newpaths[pc] = index = @constellation.Index(:new, composite: mapping.root, is_unique: true, presence_constraint: pc)
|
681
|
+
if mapping.object_type.preferred_identifier == pc and
|
682
|
+
!@composition.all_full_absorption[mapping.object_type] and
|
683
|
+
!mapping.root.natural_index
|
684
|
+
mapping.root.natural_index = index
|
685
|
+
mapping.root.primary_index ||= index # Not if we have a surrogate already
|
686
|
+
end
|
687
|
+
trace :relational_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}"
|
688
|
+
end
|
689
|
+
end
|
690
|
+
newpaths
|
587
691
|
end
|
588
692
|
|
589
693
|
def relevant_paths path_hash, component
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
694
|
+
rel = {} # REVISIT: return a hash subset of path_hash containing paths relevant to this component
|
695
|
+
case component
|
696
|
+
when MM::Absorption
|
697
|
+
role = component.child_role.base_role
|
698
|
+
when MM::Indicator
|
699
|
+
role = component.role
|
700
|
+
else
|
701
|
+
return rel # Can't participate in an AccessPath
|
702
|
+
end
|
703
|
+
|
704
|
+
path_hash.each do |pc, path|
|
705
|
+
next unless pc.role_sequence.all_role_ref.detect{|rr| rr.role == role}
|
706
|
+
rel[pc] = path
|
707
|
+
end
|
708
|
+
rel
|
605
709
|
end
|
606
710
|
|
607
711
|
def augment_paths paths, mapping
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
712
|
+
return unless MM::Indicator === mapping || MM::ValueType === mapping.object_type
|
713
|
+
|
714
|
+
if MM::ValueField === mapping && mapping.parent.composite # ValueType that's a composite (table) by itself
|
715
|
+
# This AccessPath has exactly one field and no presence constraint, so just make the index.
|
716
|
+
composite = mapping.parent.composite
|
717
|
+
paths[nil] =
|
718
|
+
index = @constellation.Index(:new, composite: mapping.root, is_unique: true, presence_constraint: nil, composite_as_natural_index: composite)
|
719
|
+
composite.primary_index ||= index
|
720
|
+
end
|
721
|
+
|
722
|
+
paths.each do |pc, path|
|
723
|
+
trace :relational_paths, "Adding #{mapping.inspect} to #{path.inspect}" do
|
724
|
+
case path
|
725
|
+
when MM::Index
|
726
|
+
@constellation.IndexField(access_path: path, ordinal: path.all_index_field.size, component: mapping)
|
727
|
+
when MM::ForeignKey
|
728
|
+
@constellation.ForeignKeyField(foreign_key: path, ordinal: path.all_foreign_key_field.size, component: mapping)
|
729
|
+
end
|
730
|
+
end
|
731
|
+
end
|
626
732
|
end
|
627
733
|
|
628
734
|
def complete_foreign_keys
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
735
|
+
trace :relational_paths, "Completing foreign keys" do
|
736
|
+
@composition.all_composite.each do |composite|
|
737
|
+
composite.all_access_path.each do |path|
|
738
|
+
next if MM::Index === path
|
739
|
+
|
740
|
+
next if path.all_foreign_key_field.size == path.all_index_field.size
|
741
|
+
target_object_type = path.absorption.child_role.object_type
|
742
|
+
while fa = target_object_type.all_full_absorption[@composition]
|
743
|
+
target_object_type = fa.absorption.parent_role.object_type
|
744
|
+
end
|
745
|
+
target = @composites[target_object_type]
|
746
|
+
prefer_natural = prefer_natural_key(false, composite, target)
|
747
|
+
trace :relational_paths, "Completing #{path.inspect} to #{target.mapping.inspect}"
|
748
|
+
index = (prefer_natural && target.natural_index) || target.primary_index
|
749
|
+
if index
|
750
|
+
index.all_index_field.each do |index_field|
|
751
|
+
@constellation.IndexField access_path: path, ordinal: index_field.ordinal, component: index_field.component
|
752
|
+
end
|
753
|
+
else
|
754
|
+
raise "Foreign key from #{path.source_composite.mapping.name} references target table #{target.mapping.name} which has no primary index"
|
755
|
+
end
|
756
|
+
end
|
757
|
+
end
|
758
|
+
end
|
650
759
|
end
|
651
760
|
|
652
761
|
def fork_component_to_new_parent parent, component
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
762
|
+
case component
|
763
|
+
# A place to put more special cases.
|
764
|
+
when MM::ValueField
|
765
|
+
# When we fork from a ValueField, we want to use the name of the ValueType, not the ValueField name
|
766
|
+
@constellation.fork component, guid: :new, parent: parent, name: component.object_type.name
|
767
|
+
else
|
768
|
+
@constellation.fork component, guid: :new, parent: parent
|
769
|
+
end
|
661
770
|
end
|
662
771
|
|
663
772
|
# Make a new Absorption in the reverse direction from the one given
|
664
773
|
def mirror absorption, parent
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
774
|
+
@constellation.fork(
|
775
|
+
absorption,
|
776
|
+
guid: :new,
|
777
|
+
object_type: absorption.parent_role.object_type,
|
778
|
+
parent: parent,
|
779
|
+
parent_role: absorption.child_role,
|
780
|
+
child_role: absorption.parent_role,
|
781
|
+
ordinal: 0,
|
782
|
+
name: role_name(absorption.parent_role)
|
783
|
+
)
|
675
784
|
end
|
676
785
|
|
677
786
|
# A candidate is a Mapping of an object type which may become a Composition (a table, in relational-speak)
|
678
787
|
class Candidate
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
|
711
|
-
|
712
|
-
|
713
|
-
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
718
|
-
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
|
728
|
-
|
729
|
-
|
730
|
-
|
731
|
-
|
732
|
-
|
733
|
-
|
734
|
-
|
735
|
-
|
736
|
-
|
737
|
-
|
738
|
-
|
739
|
-
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
|
755
|
-
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
|
760
|
-
|
761
|
-
|
762
|
-
|
763
|
-
|
764
|
-
|
765
|
-
|
766
|
-
|
767
|
-
|
768
|
-
|
769
|
-
|
770
|
-
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
-
|
779
|
-
|
780
|
-
|
781
|
-
|
782
|
-
|
783
|
-
|
784
|
-
|
785
|
-
|
786
|
-
|
787
|
-
|
788
|
-
|
789
|
-
|
790
|
-
|
791
|
-
|
792
|
-
|
793
|
-
|
794
|
-
|
795
|
-
|
796
|
-
|
797
|
-
|
798
|
-
|
788
|
+
attr_reader :mapping, :is_table, :is_tentative
|
789
|
+
attr_accessor :full_absorption
|
790
|
+
|
791
|
+
def initialize compositor, mapping
|
792
|
+
@compositor = compositor
|
793
|
+
@mapping = mapping
|
794
|
+
end
|
795
|
+
|
796
|
+
def object_type
|
797
|
+
@mapping.object_type
|
798
|
+
end
|
799
|
+
|
800
|
+
# References from us are things we can own (non-Mappings) or have a unique forward absorption for
|
801
|
+
def references_from
|
802
|
+
@mapping.all_member.select{|m| !m.is_a?(MM::Absorption) or !m.forward_absorption && m.parent_role.is_unique }
|
803
|
+
end
|
804
|
+
alias_method :rf, :references_from
|
805
|
+
|
806
|
+
# References to us are reverse absorptions where the forward absorption can absorb us
|
807
|
+
def references_to
|
808
|
+
@mapping.all_member.select{|m| m.is_a?(MM::Absorption) and f = m.forward_absorption and f.parent_role.is_unique}
|
809
|
+
end
|
810
|
+
alias_method :rt, :references_to
|
811
|
+
|
812
|
+
def has_references
|
813
|
+
@mapping.all_member.select{|m| m.is_a?(MM::Absorption) }
|
814
|
+
end
|
815
|
+
|
816
|
+
def definitely_not_table
|
817
|
+
@is_tentative = @is_table = false
|
818
|
+
end
|
819
|
+
|
820
|
+
def definitely_table
|
821
|
+
@is_tentative = false
|
822
|
+
@is_table = true
|
823
|
+
end
|
824
|
+
|
825
|
+
def probably_not_table
|
826
|
+
@is_tentative = true
|
827
|
+
@is_table = false
|
828
|
+
end
|
829
|
+
|
830
|
+
def probably_table
|
831
|
+
@is_tentative = @is_table = true
|
832
|
+
end
|
833
|
+
|
834
|
+
def assign_default composition
|
835
|
+
o = object_type
|
836
|
+
if o.is_separate
|
837
|
+
trace :relational_defaults, "#{o.name} is a table because it's declared independent or separate"
|
838
|
+
definitely_table
|
839
|
+
return
|
840
|
+
end
|
841
|
+
|
842
|
+
case o
|
843
|
+
when MM::ValueType
|
844
|
+
if o.is_auto_assigned
|
845
|
+
trace :relational_defaults, "#{o.name} is not a table because it is auto assigned"
|
846
|
+
definitely_not_table
|
847
|
+
elsif references_from.size > 0
|
848
|
+
trace :relational_defaults, "#{o.name} is a table because it has references to absorb"
|
849
|
+
definitely_table
|
850
|
+
else
|
851
|
+
trace :relational_defaults, "#{o.name} is not a table because it will be absorbed wherever needed"
|
852
|
+
definitely_not_table
|
853
|
+
end
|
854
|
+
|
855
|
+
when MM::EntityType
|
856
|
+
if references_to.empty? and
|
857
|
+
!references_from.detect do |absorption| # detect whether anything can absorb this entity type
|
858
|
+
absorption.is_a?(MM::Mapping) && absorption.parent_role.is_unique && absorption.child_role.is_unique
|
859
|
+
end
|
860
|
+
trace :relational_defaults, "#{o.name} is a table because it has nothing to absorb it"
|
861
|
+
definitely_table
|
862
|
+
return
|
863
|
+
end
|
864
|
+
if !o.supertypes.empty?
|
865
|
+
# We know that this entity type is not a separate or partitioned subtype, so a supertype that can absorb us does
|
866
|
+
identifying_fact_type = o.all_type_inheritance_as_subtype.detect{|ti| ti.provides_identification}
|
867
|
+
if identifying_fact_type
|
868
|
+
fact_type = identifying_fact_type
|
869
|
+
else
|
870
|
+
if o.all_type_inheritance_as_subtype.size > 1
|
871
|
+
trace :relational_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)"
|
872
|
+
end
|
873
|
+
fact_type = o.all_type_inheritance_as_subtype.to_a[0]
|
874
|
+
end
|
875
|
+
|
876
|
+
absorbing_ref = mapping.all_member.detect{|m| m.is_a?(MM::Absorption) && m.child_role.fact_type == fact_type}
|
877
|
+
|
878
|
+
absorbing_ref = absorbing_ref.flip! if absorbing_ref.reverse_absorption # We were forward, but the other end must be
|
879
|
+
absorbing_ref = absorbing_ref.forward_absorption
|
880
|
+
self.full_absorption =
|
881
|
+
o.constellation.FullAbsorption(composition: composition, absorption: absorbing_ref, object_type: o)
|
882
|
+
trace :relational_defaults, "Supertype #{fact_type.supertype_role.name} fully absorbs subtype #{o.name} via #{absorbing_ref.inspect}"
|
883
|
+
definitely_not_table
|
884
|
+
return
|
885
|
+
end # subtype
|
886
|
+
|
887
|
+
# If the preferred_identifier consists of a ValueType that's auto-assigned ON COMMIT (like an SQL sequence),
|
888
|
+
# that can only happen in one table, which controls the sequence.
|
889
|
+
auto_assigned_identifying_role_player = nil
|
890
|
+
pi_role_refs = o.preferred_identifier.role_sequence.all_role_ref
|
891
|
+
if pi_role_refs.size == 1 and
|
892
|
+
rr = pi_role_refs.single and
|
893
|
+
(v = rr.role.object_type).is_a?(MM::ValueType) and
|
894
|
+
v.is_auto_assigned == 'commit'
|
895
|
+
auto_assigned_identifying_role_player = v
|
896
|
+
end
|
897
|
+
if (@compositor.options['single_sequence'] || references_to.size > 1) and auto_assigned_identifying_role_player # Can be absorbed in more than one place
|
898
|
+
trace :relational_defaults, "#{o.name} must be a table to support its auto-assigned identifier #{auto_assigned_identifying_role_player.name}"
|
899
|
+
definitely_table
|
900
|
+
return
|
901
|
+
end
|
902
|
+
|
903
|
+
trace :relational_defaults, "#{o.name} is initially presumed to be a table"
|
904
|
+
probably_table
|
905
|
+
|
906
|
+
end # case
|
907
|
+
end
|
799
908
|
|
800
909
|
end
|
801
910
|
|