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.
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