activefacts 0.6.0
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.
- data/History.txt +4 -0
- data/Manifest.txt +83 -0
- data/README.rdoc +81 -0
- data/Rakefile +41 -0
- data/bin/afgen +46 -0
- data/bin/cql +52 -0
- data/examples/CQL/Address.cql +46 -0
- data/examples/CQL/Blog.cql +54 -0
- data/examples/CQL/CompanyDirectorEmployee.cql +51 -0
- data/examples/CQL/Death.cql +16 -0
- data/examples/CQL/Genealogy.cql +95 -0
- data/examples/CQL/Marriage.cql +18 -0
- data/examples/CQL/Metamodel.cql +238 -0
- data/examples/CQL/MultiInheritance.cql +19 -0
- data/examples/CQL/OilSupply.cql +47 -0
- data/examples/CQL/Orienteering.cql +108 -0
- data/examples/CQL/PersonPlaysGame.cql +17 -0
- data/examples/CQL/SchoolActivities.cql +31 -0
- data/examples/CQL/SimplestUnary.cql +12 -0
- data/examples/CQL/SubtypePI.cql +32 -0
- data/examples/CQL/Warehousing.cql +99 -0
- data/examples/CQL/WindowInRoomInBldg.cql +22 -0
- data/lib/activefacts.rb +10 -0
- data/lib/activefacts/api.rb +25 -0
- data/lib/activefacts/api/concept.rb +384 -0
- data/lib/activefacts/api/constellation.rb +106 -0
- data/lib/activefacts/api/entity.rb +239 -0
- data/lib/activefacts/api/instance.rb +54 -0
- data/lib/activefacts/api/numeric.rb +158 -0
- data/lib/activefacts/api/role.rb +94 -0
- data/lib/activefacts/api/standard_types.rb +67 -0
- data/lib/activefacts/api/support.rb +59 -0
- data/lib/activefacts/api/value.rb +122 -0
- data/lib/activefacts/api/vocabulary.rb +120 -0
- data/lib/activefacts/cql.rb +31 -0
- data/lib/activefacts/cql/CQLParser.treetop +104 -0
- data/lib/activefacts/cql/Concepts.treetop +112 -0
- data/lib/activefacts/cql/DataTypes.treetop +66 -0
- data/lib/activefacts/cql/Expressions.treetop +113 -0
- data/lib/activefacts/cql/FactTypes.treetop +185 -0
- data/lib/activefacts/cql/Language/English.treetop +92 -0
- data/lib/activefacts/cql/LexicalRules.treetop +169 -0
- data/lib/activefacts/cql/Rakefile +6 -0
- data/lib/activefacts/cql/parser.rb +88 -0
- data/lib/activefacts/generate/absorption.rb +87 -0
- data/lib/activefacts/generate/cql.rb +441 -0
- data/lib/activefacts/generate/cql/html.rb +397 -0
- data/lib/activefacts/generate/null.rb +19 -0
- data/lib/activefacts/generate/ordered.rb +557 -0
- data/lib/activefacts/generate/ruby.rb +326 -0
- data/lib/activefacts/generate/sql/server.rb +164 -0
- data/lib/activefacts/generate/text.rb +21 -0
- data/lib/activefacts/input/cql.rb +1268 -0
- data/lib/activefacts/input/orm.rb +926 -0
- data/lib/activefacts/persistence.rb +1 -0
- data/lib/activefacts/persistence/composition.rb +653 -0
- data/lib/activefacts/support.rb +51 -0
- data/lib/activefacts/version.rb +3 -0
- data/lib/activefacts/vocabulary.rb +6 -0
- data/lib/activefacts/vocabulary/extensions.rb +343 -0
- data/lib/activefacts/vocabulary/metamodel.rb +303 -0
- data/script/txt2html +71 -0
- data/spec/absorption_spec.rb +95 -0
- data/spec/api/autocounter.rb +82 -0
- data/spec/api/constellation.rb +130 -0
- data/spec/api/entity_type.rb +101 -0
- data/spec/api/instance.rb +428 -0
- data/spec/api/roles.rb +122 -0
- data/spec/api/value_type.rb +112 -0
- data/spec/api_spec.rb +14 -0
- data/spec/cql_cql_spec.rb +58 -0
- data/spec/cql_parse_spec.rb +31 -0
- data/spec/cql_ruby_spec.rb +60 -0
- data/spec/cql_sql_spec.rb +54 -0
- data/spec/cql_symbol_tables_spec.rb +259 -0
- data/spec/cql_unit_spec.rb +336 -0
- data/spec/cqldump_spec.rb +169 -0
- data/spec/norma_cql_spec.rb +48 -0
- data/spec/norma_ruby_spec.rb +50 -0
- data/spec/norma_sql_spec.rb +45 -0
- data/spec/norma_tables_spec.rb +94 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +10 -0
- metadata +173 -0
@@ -0,0 +1 @@
|
|
1
|
+
require 'activefacts/persistence/composition'
|
@@ -0,0 +1,653 @@
|
|
1
|
+
#
|
2
|
+
# Calculate the relational composition of a given Vocabulary
|
3
|
+
# The composition consists of decisiona about which Concepts are tables,
|
4
|
+
# and what columns (absorbed roled) those tables will have.
|
5
|
+
#
|
6
|
+
# This module has the following known problems:
|
7
|
+
#
|
8
|
+
# * Some one-to-ones absorb in both directions (ET<->FT in Metamodel, Blog model)
|
9
|
+
#
|
10
|
+
# * When a subtype has no mandatory roles, we should introduce
|
11
|
+
# a binary (is_subtype) to indicate it's that subtype.
|
12
|
+
#
|
13
|
+
module ActiveFacts
|
14
|
+
module Metamodel
|
15
|
+
class Concept
|
16
|
+
def absorbed_references
|
17
|
+
absorbed_roles # Calculate the list if not done already
|
18
|
+
@absorbed_references
|
19
|
+
end
|
20
|
+
|
21
|
+
# Return a RoleSequence containing a RoleRef (with JoinPath) for every column
|
22
|
+
# The vocabulary must have first been composed by calling "tables".
|
23
|
+
def absorbed_roles
|
24
|
+
if @absorbed_roles
|
25
|
+
# Recursion guard
|
26
|
+
raise "infinite absorption loop on #{name}" if @evaluating
|
27
|
+
return @absorbed_roles
|
28
|
+
end
|
29
|
+
@absorbed_references = []
|
30
|
+
rs = RoleSequence.new(:new)
|
31
|
+
@evaluating = true
|
32
|
+
|
33
|
+
# REVISIT: Emit preferred identifier roles first.
|
34
|
+
# Care though; an independent subtype absorbs a reference to its superclass, not the preferred_identifier roles
|
35
|
+
inject_value_type_role = is_a?(ValueType)
|
36
|
+
|
37
|
+
debug :absorption, "absorbed_roles of #{name} are:" do
|
38
|
+
can_absorb.each do |role|
|
39
|
+
other_player =
|
40
|
+
case
|
41
|
+
when role.fact_type.all_role.size == 1; nil
|
42
|
+
when !role.fact_type.entity_type || role.fact_type.entity_type == self; role.concept
|
43
|
+
else role.fact_type.entity_type
|
44
|
+
end
|
45
|
+
|
46
|
+
# When a ValueType is independent, it always absorbs another ValueType or has a unary role.
|
47
|
+
# If this is an absorbed VT, it's our chance to also define the value role for this ValueType.
|
48
|
+
if (inject_value_type_role && other_player.is_a?(ValueType))
|
49
|
+
my_role = (role.fact_type.all_role-[role])[0]
|
50
|
+
rr = my_role.preferred_reference.append_to(rs)
|
51
|
+
rr.trailing_adjective = "#{rr.trailing_adjective}Value"
|
52
|
+
inject_value_type_role = false
|
53
|
+
end
|
54
|
+
|
55
|
+
# If the role is unary, or independent, or what we're referring is absorbed elsewhere, emit a reference:
|
56
|
+
reference_only = !other_player ||
|
57
|
+
other_player.independent ||
|
58
|
+
(other_player.is_a?(EntityType) and (via = other_player.absorbed_via) and via != role.fact_type)
|
59
|
+
|
60
|
+
debug :absorption, "#{name} absorbs #{reference_only ? "reference" : "all"} roles#{(other_player && " of "+other_player.name)} because '#{role.fact_type.default_reading}' via #{via && via.describe(role)} #{
|
61
|
+
#role.preferred_reference.describe
|
62
|
+
role.fact_type.describe(role)
|
63
|
+
}" do
|
64
|
+
if reference_only
|
65
|
+
f, t, @from_columns, @to_columns = @from_columns, @to_columns, nil, nil
|
66
|
+
@absorbed_references << [role, other_player, @from_columns = [], @to_columns = []] if other_player
|
67
|
+
absorb_reference(rs, role)
|
68
|
+
@from_columns, @to_columns = f, t
|
69
|
+
# Objectified Unaries may play additional roles that were't in can_absorb:
|
70
|
+
absorb_entity_roles(rs, role.fact_type.entity_type, role) if (!other_player && role.fact_type.entity_type)
|
71
|
+
else
|
72
|
+
absorb_all_roles(rs, role)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# If the ValueType is independent only because it has a unary role,
|
78
|
+
# there is no role to absorb, duuh.
|
79
|
+
# REVISIT: define the value role for this ValueType.
|
80
|
+
if (inject_value_type_role)
|
81
|
+
# my_role = (role.fact_type.all_role-[role])[0]
|
82
|
+
# rr = my_role.preferred_reference.append_to(rs)
|
83
|
+
# rr.trailing_adjective = "#{rr.trailing_adjective}Value"
|
84
|
+
# inject_value_type_role = false
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
|
89
|
+
# Now go through all absorbed roles and ensure they have distinct names
|
90
|
+
roles_by_preferred_name =
|
91
|
+
rs.all_role_ref.inject({}) do |h, rr|
|
92
|
+
role = (jp = rr.all_join_path[0]) ? jp.input_role : rr.role
|
93
|
+
name = role.preferred_reference.role_name(".")
|
94
|
+
(h[name] ||= {})[role] = rr
|
95
|
+
h
|
96
|
+
end
|
97
|
+
roles_by_preferred_name.
|
98
|
+
keys. # Select the preferred names that attach to more than one role
|
99
|
+
select{|n| roles_by_preferred_name[n].size > 1}.
|
100
|
+
each do |n|
|
101
|
+
roles = roles_by_preferred_name[n].keys
|
102
|
+
# puts "REVISIT: #{name} has #{roles.size} roles named #{n}"
|
103
|
+
end
|
104
|
+
|
105
|
+
@evaluating = false
|
106
|
+
@absorbed_roles = rs
|
107
|
+
@absorbed_roles
|
108
|
+
end
|
109
|
+
|
110
|
+
# Return the array of absorption paths (roles of this object) that could absorb this object or a reference to it
|
111
|
+
def absorption_paths
|
112
|
+
return @absorption_paths if @absorption_paths
|
113
|
+
@absorption_paths =
|
114
|
+
all_role.map do |role|
|
115
|
+
role_type = role.role_type
|
116
|
+
case role_type
|
117
|
+
when :supertype, # Never absorb a supertype into its subtype (REVISIT: until later when we support partitioning)
|
118
|
+
:many_one # Can't absorb many of these into one of those
|
119
|
+
next nil
|
120
|
+
when :unary
|
121
|
+
next nil # Never absorb an object into one if its unaries
|
122
|
+
when :subtype, # This object is a subtype, so can be absorbed. REVISIT: Support subtype separation and partition
|
123
|
+
:one_many
|
124
|
+
next role
|
125
|
+
when :one_one # This object
|
126
|
+
# Never absorb an entity type into a value type:
|
127
|
+
next nil if ValueType === role.other_role_player and !is_a?(ValueType)
|
128
|
+
next role
|
129
|
+
else
|
130
|
+
raise "Illegal role type, #{role.fact_type.describe(role)} no uniqueness constraint"
|
131
|
+
end
|
132
|
+
end.compact
|
133
|
+
end
|
134
|
+
|
135
|
+
# Return the Concept into which this concept would be absorbed through its role given
|
136
|
+
def referenced_from(role)
|
137
|
+
(self == role.fact_type.entity_type && role.concept) || # It's a role of this objectified FT
|
138
|
+
role.fact_type.entity_type || # This is a role in another objectified FT
|
139
|
+
(role.fact_type.all_role-[role])[0].concept # A normal role played by this concept in a binary FT
|
140
|
+
end
|
141
|
+
|
142
|
+
# Return a RoleSequence with RoleRefs (including JoinPath) for all ValueTypes required to form this EntityType's preferred_identifier
|
143
|
+
def absorbed_reference_roles
|
144
|
+
rs = RoleSequence.new(:new)
|
145
|
+
debug :absorption, "absorbed_reference_roles of #{name} are:" do
|
146
|
+
reference_roles.all_role_ref.each do |rr|
|
147
|
+
debug :absorption, "absorbed_reference_role of #{name} is #{rr.role.fact_type.describe(rr.role)}"
|
148
|
+
f, t = @from_columns, @to_columns
|
149
|
+
absorb_reference(rs, rr.role)
|
150
|
+
@from_columns, @to_columns = f, t
|
151
|
+
end
|
152
|
+
end
|
153
|
+
rs
|
154
|
+
end
|
155
|
+
|
156
|
+
# This object is related to a Concept by this role played by that Concept.
|
157
|
+
# If it's a ValueType, add the role to this RoleSequence,
|
158
|
+
# otherwise add the reference roles for that EntityType.
|
159
|
+
# Note that the role may be in an objectified fact type,
|
160
|
+
# at either end (self or role.concept).
|
161
|
+
def absorb_reference(rs, role)
|
162
|
+
if role.concept.is_a? ValueType
|
163
|
+
role.preferred_reference.append_to(rs)
|
164
|
+
elsif role.fact_type.entity_type != self and role.fact_type.all_role.size == 1
|
165
|
+
# A unary fact type, just add it:
|
166
|
+
return RoleRef.new(rs, rs.all_role_ref.size+1, :role => role)
|
167
|
+
else
|
168
|
+
# Add this role as a JoinPath to the referenced object's absorbed_reference_roles
|
169
|
+
debug :absorption, "Absorbing reference to #{role.concept.name} into #{name}" do
|
170
|
+
absorbed_rs = role.concept.absorbed_reference_roles
|
171
|
+
absorbed_rs.all_role_ref.each do |rr|
|
172
|
+
# Figure out what concept is traversed by the new JoinPath:
|
173
|
+
concept = (role.concept == self && role.fact_type.entity_type) || role.concept
|
174
|
+
new_rr = extend_join_path(rs, rr, role, concept)
|
175
|
+
@to_columns << rr if @to_columns
|
176
|
+
@from_columns << new_rr if @to_columns
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# Absorb (into the RoleSequence) all roles that are absorbed by the player of this role
|
183
|
+
def absorb_all_roles(rs, role)
|
184
|
+
#debug :absorption, "absorb_all_roles of #{role.fact_type.describe(role)}"
|
185
|
+
|
186
|
+
if role.concept.is_a? ValueType # Absorb a role played by a ValueType
|
187
|
+
role.preferred_reference.append_to(rs)
|
188
|
+
elsif role.fact_type.entity_type != self and role.fact_type.all_role.size == 1
|
189
|
+
# Absorb a unary role:
|
190
|
+
return RoleRef.new(rs, rs.all_role_ref.size+1, :role => role)
|
191
|
+
else
|
192
|
+
player = role.fact_type.entity_type
|
193
|
+
player = role.concept if !player || player == self
|
194
|
+
if player.independent
|
195
|
+
f, t = @from_columns, @to_columns
|
196
|
+
absorb_reference(rs, role)
|
197
|
+
@from_columns, @to_columns = f, t
|
198
|
+
else
|
199
|
+
absorb_entity_roles(rs, player, role)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def absorb_entity_roles(rs, entity_type, role)
|
205
|
+
absorbed_rs = entity_type.absorbed_roles
|
206
|
+
absorbed_rs.all_role_ref.each do |rr|
|
207
|
+
# Figure out what concept is traversed by the new JoinPath:
|
208
|
+
concept = role.concept == self ? role.fact_type.entity_type : role.concept
|
209
|
+
new_rr = extend_join_path(rs, rr, role, concept)
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
# Copy the RoleRef into the RoleSequence, prepending a JoinPath via role and concept to the copy
|
214
|
+
def extend_join_path(rs, role_ref, role, concept)
|
215
|
+
# Copy the RoleRef and add the new one to this RoleSequence:
|
216
|
+
new_rr = role_ref.append_to(rs)
|
217
|
+
|
218
|
+
# Prepend a new JoinPath to this RoleRef.
|
219
|
+
# A JoinPath identifies two roles played by the passed concept, an input role and an output role.
|
220
|
+
# The final output role is the counterpart to the RoleRef role, played by the target.
|
221
|
+
# We're building the path in reverse order, so the role passed in is a new input role.
|
222
|
+
# Find the output role.
|
223
|
+
entry_role = (first_jp = role_ref.all_join_path.first) ? first_jp.input_role : new_rr.role
|
224
|
+
|
225
|
+
# If concept is an objectified fact type, the entry_role might be one of its roles
|
226
|
+
output_role = concept.fact_type ? entry_role : (entry_role.fact_type.all_role-[entry_role])[0]
|
227
|
+
|
228
|
+
# REVISIT: For an input_role in a unary fact_type, output_role will be nil (in case this is a problem)
|
229
|
+
JoinPath.new(new_rr, 0, :concept => concept, :input_role => role, :output_role => output_role)
|
230
|
+
|
231
|
+
# Append the old JoinPaths if any
|
232
|
+
role_ref.all_join_path.each do |jp|
|
233
|
+
JoinPath.new(new_rr, new_rr.all_join_path.size, :concept => jp.concept, :input_role => jp.input_role, :output_role => jp.output_role)
|
234
|
+
end
|
235
|
+
new_rr
|
236
|
+
end
|
237
|
+
|
238
|
+
# can_absorb is an array of roles of other Concepts that this concept can absorb
|
239
|
+
# It may include roles of concepts into which this one may be absorbed, until we decide which way to go.
|
240
|
+
def can_absorb
|
241
|
+
@can_absorb ||= []
|
242
|
+
end
|
243
|
+
|
244
|
+
# Say whether the independence of this object is still under consideration
|
245
|
+
# This is used in detecting dependency cycles, such as occurs in the Metamodel
|
246
|
+
attr_accessor :tentative
|
247
|
+
attr_writer :independent
|
248
|
+
end
|
249
|
+
|
250
|
+
class ValueType
|
251
|
+
# Say whether this object is currently considered independent or not:
|
252
|
+
def independent
|
253
|
+
return @independent if @independent != nil
|
254
|
+
|
255
|
+
# Always independent if marked so:
|
256
|
+
if is_independent
|
257
|
+
@tentative = false
|
258
|
+
return @independent = true
|
259
|
+
end
|
260
|
+
|
261
|
+
# Never independent unless they can absorb another ValueType or are marked is_independent
|
262
|
+
if (can_absorb.detect{|role| !role.fact_type.entity_type and role.concept.is_a? ValueType })
|
263
|
+
@tentative = true
|
264
|
+
@independent = true # Possibly independent
|
265
|
+
else
|
266
|
+
@tentative = false
|
267
|
+
@independent = false
|
268
|
+
end
|
269
|
+
|
270
|
+
@independent
|
271
|
+
end
|
272
|
+
|
273
|
+
def reference_roles
|
274
|
+
# We must be independent, so inject the self-role
|
275
|
+
rs = RoleSequence.new(:new)
|
276
|
+
role_ref = absorbed_roles.all_role_ref.detect{|rr|
|
277
|
+
rr.role.fact_type.all_role.size != 1 && rr.role.concept == self
|
278
|
+
}
|
279
|
+
# This fails if the only absorbed role is unary, or the ValueType is merely marked independent
|
280
|
+
raise "REVISIT: Can't find self-role for #{name}" unless role_ref
|
281
|
+
rr = role_ref.append_to(rs)
|
282
|
+
# REVISIT: This fails to append the Value adjective:
|
283
|
+
rr.trailing_adjective = "#{rr.trailing_adjective}Value"
|
284
|
+
rs
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
class EntityType
|
289
|
+
# Return a RoleSequence containing the preferred reference to each Role in this object's preferred_identifier
|
290
|
+
def reference_roles
|
291
|
+
rs = RoleSequence.new(:new)
|
292
|
+
preferred_identifier.role_sequence.all_role_ref.each do |rr|
|
293
|
+
rr.role.preferred_reference.append_to(rs)
|
294
|
+
end
|
295
|
+
rs
|
296
|
+
end
|
297
|
+
|
298
|
+
def absorption_paths
|
299
|
+
return @absorption_paths if @absorption_paths
|
300
|
+
super
|
301
|
+
if (fact_type)
|
302
|
+
@absorption_paths += fact_type.all_role.map do |fact_role|
|
303
|
+
# Perhaps this objectified fact type can be absorbed through one of its roles
|
304
|
+
next fact_role if fact_role.all_role_ref.detect{|rr|
|
305
|
+
# Look for a UC that covers just this role
|
306
|
+
rr.role_sequence.all_role_ref.size == 1 and
|
307
|
+
rr.role_sequence.all_presence_constraint.detect { |pc|
|
308
|
+
pc.max_frequency == 1
|
309
|
+
}
|
310
|
+
}
|
311
|
+
next nil
|
312
|
+
end.compact
|
313
|
+
end
|
314
|
+
@absorption_paths
|
315
|
+
end
|
316
|
+
|
317
|
+
# Decide whether this object is currently considered independent or not:
|
318
|
+
def independent
|
319
|
+
return @independent if @independent != nil # We already make a guess or decision
|
320
|
+
|
321
|
+
@tentative = false
|
322
|
+
|
323
|
+
# Always independent if marked so or nowhere else to go:
|
324
|
+
return @independent = true if is_independent || absorption_paths.empty?
|
325
|
+
|
326
|
+
# Subtypes are not independent unless partitioned
|
327
|
+
# REVISIT: Support partitioned subtypes here
|
328
|
+
if (!supertypes.empty?)
|
329
|
+
av = all_supertype_inheritance[0]
|
330
|
+
absorbed_via(av)
|
331
|
+
return @independent = false
|
332
|
+
end
|
333
|
+
|
334
|
+
# If the preferred_identifier includes an auto_assigned ValueType
|
335
|
+
# and this object is absorbed in more than one place, we need a table
|
336
|
+
# to manage the auto-assignment.
|
337
|
+
if absorption_paths.size > 1 &&
|
338
|
+
preferred_identifier.role_sequence.all_role_ref.detect {|rr|
|
339
|
+
next false unless rr.role.concept.is_a? ValueType
|
340
|
+
# REVISIT: Find a better way to determine AutoCounters (ValueType unary role?)
|
341
|
+
rr.role.concept.supertype.name =~ /^Auto/
|
342
|
+
}
|
343
|
+
debug :absorption, "#{name} has an auto-assigned counter in its ID, so must be independent"
|
344
|
+
@tentative = false
|
345
|
+
return @independent = true
|
346
|
+
end
|
347
|
+
|
348
|
+
@tentative = true
|
349
|
+
@independent = true
|
350
|
+
end
|
351
|
+
|
352
|
+
def absorbed_via(fact_type = nil)
|
353
|
+
# puts "#{name} is absorbed via #{fact_type.describe(role)}" if role
|
354
|
+
@absorbed_via = fact_type if fact_type
|
355
|
+
@absorbed_via
|
356
|
+
end
|
357
|
+
end # EntityType class
|
358
|
+
|
359
|
+
class RoleRef
|
360
|
+
# Append a copy of this reference to this RoleSequence
|
361
|
+
def append_to(rs)
|
362
|
+
RoleRef.new(rs, rs.all_role_ref.size+1,
|
363
|
+
:role => role,
|
364
|
+
:leading_adjective => leading_adjective,
|
365
|
+
:trailing_adjective => trailing_adjective
|
366
|
+
)
|
367
|
+
end
|
368
|
+
|
369
|
+
# When the joins traverse TypeInheritance, retain the subtype join path only:
|
370
|
+
def direct_type_join_paths
|
371
|
+
all_join_path.
|
372
|
+
sort_by{|jp| jp.join_step}.
|
373
|
+
inject([]) do |a, jp|
|
374
|
+
if a.last && (ti = jp.input_role.fact_type).is_a?(TypeInheritance)
|
375
|
+
if ti.subtype == jp.input_role.concept
|
376
|
+
# Retain the subtype, not the previously-recorded supertype
|
377
|
+
a[-1] = jp
|
378
|
+
# else we already had the supertype
|
379
|
+
end
|
380
|
+
else
|
381
|
+
a << jp
|
382
|
+
end
|
383
|
+
a
|
384
|
+
end.inject([]) do |a, jp|
|
385
|
+
# Skip an object which is identified by its precursor:
|
386
|
+
next a if a.size > 0 and
|
387
|
+
jp.concept.is_a?(EntityType) and
|
388
|
+
(role_refs = jp.concept.preferred_identifier.role_sequence.all_role_ref).size == 1 and
|
389
|
+
role_refs[0].role == a[-1].output_role
|
390
|
+
|
391
|
+
## Skip a role that is a sole ValueType identifying its precursor:
|
392
|
+
# This seems to have very little effect
|
393
|
+
#next a if a.size > 0 and
|
394
|
+
# #jp.concept.is_a?(ValueType) and
|
395
|
+
# (role_refs = a[-1].concept.preferred_identifier.role_sequence.all_role_ref).size == 1 and
|
396
|
+
# role_refs[0].role == jp.input_role
|
397
|
+
|
398
|
+
a << jp
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
# Return an array of column name words
|
403
|
+
def column_name(joiner = "-")
|
404
|
+
jps = direct_type_join_paths
|
405
|
+
|
406
|
+
# When Xyz is identified by XyzID, truncate that to just ID:
|
407
|
+
final_name = role_name(nil)
|
408
|
+
if final_name.size == 1 && jps.size > 0 && role.fact_type.all_role.size > 1 # We have a JoinPath to a non-unary
|
409
|
+
penultimate_role_player = all_join_path.last.concept
|
410
|
+
final_name = final_name[0]
|
411
|
+
if penultimate_role_player.is_a?(EntityType) and
|
412
|
+
(role_refs = penultimate_role_player.preferred_identifier.role_sequence.all_role_ref).size == 1 and
|
413
|
+
role_refs[0].role == role and
|
414
|
+
final_name[0...penultimate_role_player.name.size].downcase == penultimate_role_player.name.downcase
|
415
|
+
#puts "===== #{final_name} starts with and identifies #{penultimate_role_player.name} for #{jps.last.concept.name}"
|
416
|
+
final_name = final_name[penultimate_role_player.name.size..-1]
|
417
|
+
final_name = nil if final_name == ''
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
names = (jps.map{ |jp| jp.column_name(nil) }.flatten+Array(final_name)).compact
|
422
|
+
joiner ? names*joiner : names
|
423
|
+
end
|
424
|
+
|
425
|
+
def output_roles
|
426
|
+
first_counterpart = all_join_path.size > 0 ? all_join_path[0].input_role : role
|
427
|
+
(first_counterpart.fact_type.all_role.size != 1 && !first_counterpart.fact_type.entity_type ?
|
428
|
+
first_counterpart.fact_type.all_role-[first_counterpart] : [first_counterpart]) +
|
429
|
+
all_join_path.map(&:output_role).compact
|
430
|
+
end
|
431
|
+
|
432
|
+
def describe
|
433
|
+
# The reference traverses the JoinPaths in sequence to the final role:
|
434
|
+
all_join_path.
|
435
|
+
sort_by{|jp| jp.join_step}.
|
436
|
+
map{ |jp| jp.describe }.compact.map{|n| n+".\n\t\t"}*"" + role_name(".")
|
437
|
+
end
|
438
|
+
end
|
439
|
+
|
440
|
+
class Role
|
441
|
+
def role_type
|
442
|
+
# TypeInheritance roles are always 1:1
|
443
|
+
if TypeInheritance === fact_type
|
444
|
+
return concept == fact_type.supertype ? :supertype : :subtype
|
445
|
+
end
|
446
|
+
|
447
|
+
# Always N:1 if unary:
|
448
|
+
return :unary if fact_type.all_role.size == 1
|
449
|
+
|
450
|
+
# List the UCs on this fact type:
|
451
|
+
all_uniqueness_constraints =
|
452
|
+
fact_type.all_role.map do |fact_role|
|
453
|
+
fact_role.all_role_ref.map do |rr|
|
454
|
+
rr.role_sequence.all_presence_constraint.select do |pc|
|
455
|
+
pc.max_frequency == 1
|
456
|
+
end
|
457
|
+
end
|
458
|
+
end.flatten.uniq
|
459
|
+
|
460
|
+
to_1 =
|
461
|
+
all_uniqueness_constraints.
|
462
|
+
detect do |c|
|
463
|
+
c.role_sequence.all_role_ref.size == 1 and
|
464
|
+
c.role_sequence.all_role_ref[0].role == self
|
465
|
+
end
|
466
|
+
|
467
|
+
if fact_type.entity_type
|
468
|
+
# This is a role in an objectified fact type
|
469
|
+
from_1 = true
|
470
|
+
else
|
471
|
+
# It's to-1 if a UC exists over roles of this FT that doesn't cover this role:
|
472
|
+
from_1 = all_uniqueness_constraints.detect{|uc|
|
473
|
+
!uc.role_sequence.all_role_ref.detect{|rr| rr.role == self || rr.role.fact_type != fact_type}
|
474
|
+
}
|
475
|
+
end
|
476
|
+
|
477
|
+
if from_1
|
478
|
+
return to_1 ? :one_one : :one_many
|
479
|
+
else
|
480
|
+
return to_1 ? :many_one : :many_many
|
481
|
+
end
|
482
|
+
end
|
483
|
+
|
484
|
+
# Each Role of an objectified fact type has no counterpart role; the other player is the objectifying entity.
|
485
|
+
# Otherwise return the player of the other role in a binary fact types
|
486
|
+
def other_role_player
|
487
|
+
fact_type.entity_type || # Objectified fact types only have counterpart roles, no self-roles
|
488
|
+
(fact_type.all_role-[self])[0].concept # Only valid for roles in binaries (others must be objectified anyhow)
|
489
|
+
end
|
490
|
+
|
491
|
+
def is_mandatory
|
492
|
+
return true if fact_type.all_role.size == 1 # Unaries are always optional, but represented as booleans, so mandatory yes/no
|
493
|
+
return true if fact_type.entity_type # Objectified fact type roles are always mandatory
|
494
|
+
all_role_ref.each { |rr|
|
495
|
+
rr.role_sequence.all_role_ref.size == 1 &&
|
496
|
+
rr.role_sequence.all_presence_constraint.each { |pc|
|
497
|
+
return pc if pc.min_frequency == 1 && pc.is_mandatory
|
498
|
+
}
|
499
|
+
}
|
500
|
+
false
|
501
|
+
end
|
502
|
+
end
|
503
|
+
|
504
|
+
class Vocabulary
|
505
|
+
# return an Array of Concepts that will have their own tables
|
506
|
+
def tables
|
507
|
+
# Strategy:
|
508
|
+
# 1) Calculate absorption paths for all Concepts
|
509
|
+
# a. Build the can_absorb list for each Concept (include unaries!)
|
510
|
+
# - Each entry must absorb either a reference or all roles (unless one-to-one; absorption may be either way)
|
511
|
+
# 2) Decide which Concepts must be and must not be tables
|
512
|
+
# a. Concepts labelled is_independent are tables
|
513
|
+
# b. Entity types having no absorption paths must be tables
|
514
|
+
# c. subtypes are not tables unless marked is_independent (subtype extension) or partitioned
|
515
|
+
# d. ValueTypes are never tables unless they can absorb other ValueTypes
|
516
|
+
# e. An EntityType having an identifying AutoInc field must be a table unless absorbed along only one path
|
517
|
+
# f. An EntityType having a preferred_identifier containing one absorption path gets absorbed
|
518
|
+
# g. An EntityType that must absorb non-PI roles must be a table unless absorbed exactly once (3NF restriction)
|
519
|
+
# h. supertypes elided if all roles are absorbed into subtypes:
|
520
|
+
# - partitioned subtype exhaustion
|
521
|
+
# - subtype extension where supertype has only PI roles and no AutoInc
|
522
|
+
# 3) Handle tentative assignments that can now be resolved
|
523
|
+
# a. Tentatively independent ValueTypes become independent if they absorb dependent ones
|
524
|
+
# b. Surely something else...?
|
525
|
+
# 4) Optimise the decision for undecided Concepts (not yet)
|
526
|
+
# a. evaluate all combinations
|
527
|
+
# b. minimise a cost function
|
528
|
+
# - cost of not absorbing = number of reference roles * number of places absorbed + number of columns in table
|
529
|
+
# - cost of absorbing = number of absorbed columns in absorbed table * number of places absorbed
|
530
|
+
# 5) Suggest improvements
|
531
|
+
# Additional cost (or inject ID?) for references to large data types (>32 bytes)
|
532
|
+
all_feature.each do |feature|
|
533
|
+
next unless feature.is_a? Concept # REVISIT: Handle Aliases here
|
534
|
+
feature.absorption_paths.each do |role|
|
535
|
+
into = feature.referenced_from(role)
|
536
|
+
# puts "#{feature.name} can be absorbed into #{into.name}"
|
537
|
+
into.can_absorb << role
|
538
|
+
end
|
539
|
+
# Ensure that all unary roles are in can_absorb also (unless objectified, already handled):
|
540
|
+
feature.all_role.select{|role|
|
541
|
+
role.fact_type.all_role.size == 1 && !role.fact_type.entity_type
|
542
|
+
}.each { |role| feature.can_absorb << role }
|
543
|
+
feature.independent = nil # Undecided
|
544
|
+
feature.tentative = nil # Undecided
|
545
|
+
end
|
546
|
+
|
547
|
+
# Evaluate the possible independence of each concept, building an array of features of indeterminate status:
|
548
|
+
undecided = []
|
549
|
+
all_feature.each do |feature|
|
550
|
+
next unless feature.is_a? Concept # REVISIT: Handle Aliases here
|
551
|
+
feature.independent
|
552
|
+
undecided << feature if (feature.tentative)
|
553
|
+
end
|
554
|
+
|
555
|
+
begin
|
556
|
+
finalised = []
|
557
|
+
undecided.each do |feature|
|
558
|
+
if feature.is_a?(ValueType) # This ValueType must be tentatively independent
|
559
|
+
# If this ValueType could absorb no independent ValueType, it must be independent (absorbs a dependendent one)
|
560
|
+
if !feature.can_absorb.detect{|role| !role.fact_type.entity_type and role.concept.independent }
|
561
|
+
feature.tentative = false
|
562
|
+
finalised << feature
|
563
|
+
end
|
564
|
+
elsif feature.is_a?(EntityType)
|
565
|
+
|
566
|
+
# Always absorb an objectified unary:
|
567
|
+
if feature.fact_type && feature.fact_type.all_role.size == 1
|
568
|
+
feature.independent = false
|
569
|
+
feature.tentative = false
|
570
|
+
finalised << feature
|
571
|
+
next
|
572
|
+
end
|
573
|
+
|
574
|
+
# If the PI contains one role only, played by an entity type that can absorb us, do that.
|
575
|
+
pi_roles = feature.preferred_identifier.role_sequence.all_role_ref.map(&:role)
|
576
|
+
if pi_roles.size == 1 &&
|
577
|
+
(into = pi_roles[0].concept).is_a?(EntityType) &&
|
578
|
+
into.absorption_paths.include?(pi_roles[0])
|
579
|
+
# This doesn't work if we already decided that "into" is fully absorbed along one path.
|
580
|
+
# It doesn't seem to be necessary anyhow.
|
581
|
+
#(into.independent || into.tentative)
|
582
|
+
|
583
|
+
feature.can_absorb.delete(pi_roles[0])
|
584
|
+
debug :absorption, "#{feature.name} absorbed along its sole reference path into #{into.name}, and reverse absorption prevented"
|
585
|
+
feature.absorbed_via(pi_roles[0].fact_type)
|
586
|
+
|
587
|
+
feature.independent = false
|
588
|
+
feature.tentative = false
|
589
|
+
finalised << feature
|
590
|
+
next
|
591
|
+
end
|
592
|
+
|
593
|
+
# If there's more than one absorption path and any functional dependencies that can't absorb us, it's independent
|
594
|
+
fd = feature.can_absorb.reject{|role| role.role_type == :one_one} - pi_roles
|
595
|
+
if (fd.size > 0)
|
596
|
+
debug :absorption, "#{feature.name} has functional dependencies so 3NF requires it be independent"
|
597
|
+
feature.independent = true
|
598
|
+
feature.tentative = false
|
599
|
+
finalised << feature
|
600
|
+
next
|
601
|
+
end
|
602
|
+
|
603
|
+
# # If there's exactly one absorption path into a object that's independent, absorb regardless of FDs
|
604
|
+
# This results in !3NF databases
|
605
|
+
# if feature.absorption_paths.size == 1 &&
|
606
|
+
# feature.absorption_paths[0].role_type != :one_one
|
607
|
+
# absorbee = feature.referenced_from(feature.absorption_paths[0])
|
608
|
+
# debug :absorption, "Absorb #{feature.name} along single path, into #{absorbee.name}"
|
609
|
+
# feature.independent = false
|
610
|
+
# feature.tentative = false
|
611
|
+
# finalised << feature
|
612
|
+
# end
|
613
|
+
|
614
|
+
# If the feature has only reference roles and any one-to-ones can absorb it, it's fully absorbed (dependent)
|
615
|
+
# We don't allow absorption into something we identify.
|
616
|
+
one_to_ones, others = (feature.can_absorb-pi_roles).partition{|role| role.role_type == :one_one }
|
617
|
+
if others.size == 0 &&
|
618
|
+
!one_to_ones.detect{|r|
|
619
|
+
player = r.fact_type.entity_type || r.concept
|
620
|
+
!player.independent ||
|
621
|
+
r.concept.is_a?(ValueType) ||
|
622
|
+
player.preferred_identifier.role_sequence.all_role_ref.map{|r2|r2.role.concept} == [feature]
|
623
|
+
}
|
624
|
+
# All one_to_ones are at least tentatively independent, make them independent and we're fully absorbed
|
625
|
+
|
626
|
+
debug :absorption, "#{feature.name} is fully absorbed, into #{one_to_ones.map{|r| r.concept.name}*", "}"
|
627
|
+
!one_to_ones.each{|role|
|
628
|
+
into = role.concept
|
629
|
+
into.tentative = false
|
630
|
+
feature.can_absorb.delete role # Things that absorb us don't want to get this role too
|
631
|
+
}
|
632
|
+
feature.independent = false
|
633
|
+
feature.tentative = false
|
634
|
+
finalised << feature
|
635
|
+
end
|
636
|
+
|
637
|
+
end
|
638
|
+
end
|
639
|
+
undecided -= finalised
|
640
|
+
end while !finalised.empty?
|
641
|
+
|
642
|
+
# Now, evaluate all possibilities of the tentative assignments
|
643
|
+
# REVISIT: Incomplete. Apparently unnecessary as well... so far.
|
644
|
+
undecided.each do |feature|
|
645
|
+
debug :absorption, "Unable to decide independence of #{feature.name}, going with #{feature.independent && "in"}dependent"
|
646
|
+
end
|
647
|
+
|
648
|
+
all_feature.select { |f| f.independent }
|
649
|
+
end
|
650
|
+
end
|
651
|
+
|
652
|
+
end
|
653
|
+
end
|