activefacts 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (84) hide show
  1. data/History.txt +4 -0
  2. data/Manifest.txt +83 -0
  3. data/README.rdoc +81 -0
  4. data/Rakefile +41 -0
  5. data/bin/afgen +46 -0
  6. data/bin/cql +52 -0
  7. data/examples/CQL/Address.cql +46 -0
  8. data/examples/CQL/Blog.cql +54 -0
  9. data/examples/CQL/CompanyDirectorEmployee.cql +51 -0
  10. data/examples/CQL/Death.cql +16 -0
  11. data/examples/CQL/Genealogy.cql +95 -0
  12. data/examples/CQL/Marriage.cql +18 -0
  13. data/examples/CQL/Metamodel.cql +238 -0
  14. data/examples/CQL/MultiInheritance.cql +19 -0
  15. data/examples/CQL/OilSupply.cql +47 -0
  16. data/examples/CQL/Orienteering.cql +108 -0
  17. data/examples/CQL/PersonPlaysGame.cql +17 -0
  18. data/examples/CQL/SchoolActivities.cql +31 -0
  19. data/examples/CQL/SimplestUnary.cql +12 -0
  20. data/examples/CQL/SubtypePI.cql +32 -0
  21. data/examples/CQL/Warehousing.cql +99 -0
  22. data/examples/CQL/WindowInRoomInBldg.cql +22 -0
  23. data/lib/activefacts.rb +10 -0
  24. data/lib/activefacts/api.rb +25 -0
  25. data/lib/activefacts/api/concept.rb +384 -0
  26. data/lib/activefacts/api/constellation.rb +106 -0
  27. data/lib/activefacts/api/entity.rb +239 -0
  28. data/lib/activefacts/api/instance.rb +54 -0
  29. data/lib/activefacts/api/numeric.rb +158 -0
  30. data/lib/activefacts/api/role.rb +94 -0
  31. data/lib/activefacts/api/standard_types.rb +67 -0
  32. data/lib/activefacts/api/support.rb +59 -0
  33. data/lib/activefacts/api/value.rb +122 -0
  34. data/lib/activefacts/api/vocabulary.rb +120 -0
  35. data/lib/activefacts/cql.rb +31 -0
  36. data/lib/activefacts/cql/CQLParser.treetop +104 -0
  37. data/lib/activefacts/cql/Concepts.treetop +112 -0
  38. data/lib/activefacts/cql/DataTypes.treetop +66 -0
  39. data/lib/activefacts/cql/Expressions.treetop +113 -0
  40. data/lib/activefacts/cql/FactTypes.treetop +185 -0
  41. data/lib/activefacts/cql/Language/English.treetop +92 -0
  42. data/lib/activefacts/cql/LexicalRules.treetop +169 -0
  43. data/lib/activefacts/cql/Rakefile +6 -0
  44. data/lib/activefacts/cql/parser.rb +88 -0
  45. data/lib/activefacts/generate/absorption.rb +87 -0
  46. data/lib/activefacts/generate/cql.rb +441 -0
  47. data/lib/activefacts/generate/cql/html.rb +397 -0
  48. data/lib/activefacts/generate/null.rb +19 -0
  49. data/lib/activefacts/generate/ordered.rb +557 -0
  50. data/lib/activefacts/generate/ruby.rb +326 -0
  51. data/lib/activefacts/generate/sql/server.rb +164 -0
  52. data/lib/activefacts/generate/text.rb +21 -0
  53. data/lib/activefacts/input/cql.rb +1268 -0
  54. data/lib/activefacts/input/orm.rb +926 -0
  55. data/lib/activefacts/persistence.rb +1 -0
  56. data/lib/activefacts/persistence/composition.rb +653 -0
  57. data/lib/activefacts/support.rb +51 -0
  58. data/lib/activefacts/version.rb +3 -0
  59. data/lib/activefacts/vocabulary.rb +6 -0
  60. data/lib/activefacts/vocabulary/extensions.rb +343 -0
  61. data/lib/activefacts/vocabulary/metamodel.rb +303 -0
  62. data/script/txt2html +71 -0
  63. data/spec/absorption_spec.rb +95 -0
  64. data/spec/api/autocounter.rb +82 -0
  65. data/spec/api/constellation.rb +130 -0
  66. data/spec/api/entity_type.rb +101 -0
  67. data/spec/api/instance.rb +428 -0
  68. data/spec/api/roles.rb +122 -0
  69. data/spec/api/value_type.rb +112 -0
  70. data/spec/api_spec.rb +14 -0
  71. data/spec/cql_cql_spec.rb +58 -0
  72. data/spec/cql_parse_spec.rb +31 -0
  73. data/spec/cql_ruby_spec.rb +60 -0
  74. data/spec/cql_sql_spec.rb +54 -0
  75. data/spec/cql_symbol_tables_spec.rb +259 -0
  76. data/spec/cql_unit_spec.rb +336 -0
  77. data/spec/cqldump_spec.rb +169 -0
  78. data/spec/norma_cql_spec.rb +48 -0
  79. data/spec/norma_ruby_spec.rb +50 -0
  80. data/spec/norma_sql_spec.rb +45 -0
  81. data/spec/norma_tables_spec.rb +94 -0
  82. data/spec/spec.opts +1 -0
  83. data/spec/spec_helper.rb +10 -0
  84. 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