activefacts 0.6.0 → 0.7.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 (35) hide show
  1. data/Manifest.txt +7 -2
  2. data/examples/CQL/Address.cql +0 -2
  3. data/examples/CQL/Blog.cql +2 -2
  4. data/examples/CQL/CompanyDirectorEmployee.cql +1 -1
  5. data/examples/CQL/Death.cql +1 -1
  6. data/examples/CQL/Metamodel.cql +5 -5
  7. data/examples/CQL/MultiInheritance.cql +2 -0
  8. data/examples/CQL/PersonPlaysGame.cql +1 -1
  9. data/lib/activefacts/cql/Concepts.treetop +17 -8
  10. data/lib/activefacts/cql/Language/English.treetop +1 -2
  11. data/lib/activefacts/generate/absorption.rb +1 -1
  12. data/lib/activefacts/generate/null.rb +8 -1
  13. data/lib/activefacts/generate/oo.rb +174 -0
  14. data/lib/activefacts/generate/ruby.rb +49 -208
  15. data/lib/activefacts/generate/sql/server.rb +137 -72
  16. data/lib/activefacts/generate/text.rb +1 -1
  17. data/lib/activefacts/input/orm.rb +12 -2
  18. data/lib/activefacts/persistence.rb +5 -1
  19. data/lib/activefacts/persistence/columns.rb +324 -0
  20. data/lib/activefacts/persistence/foreignkey.rb +87 -0
  21. data/lib/activefacts/persistence/index.rb +171 -0
  22. data/lib/activefacts/persistence/reference.rb +326 -0
  23. data/lib/activefacts/persistence/tables.rb +307 -0
  24. data/lib/activefacts/support.rb +1 -1
  25. data/lib/activefacts/version.rb +1 -1
  26. data/lib/activefacts/vocabulary/extensions.rb +42 -5
  27. data/spec/absorption_spec.rb +8 -6
  28. data/spec/cql_cql_spec.rb +1 -0
  29. data/spec/cql_sql_spec.rb +2 -1
  30. data/spec/cql_unit_spec.rb +0 -6
  31. data/spec/norma_cql_spec.rb +1 -0
  32. data/spec/norma_sql_spec.rb +1 -1
  33. data/spec/norma_tables_spec.rb +41 -43
  34. metadata +9 -4
  35. data/lib/activefacts/persistence/composition.rb +0 -653
@@ -0,0 +1,307 @@
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
+
14
+ require 'activefacts/persistence/reference'
15
+
16
+ module ActiveFacts
17
+ module Metamodel
18
+
19
+ class ValueType
20
+ def absorbed_via; nil; end # ValueTypes aren't absorbed in the way EntityTypes are
21
+
22
+ # Say whether this object is currently considered a table or not:
23
+ def is_table
24
+ return @is_table if @is_table != nil
25
+
26
+ # Always a table if marked so:
27
+ if is_independent
28
+ debug :absorption, "ValueType #{name} is declared independent"
29
+ @tentative = false
30
+ return @is_table = true
31
+ end
32
+
33
+ # Only a table if it has references (to another ValueType)
34
+ if !references_from.empty?
35
+ debug :absorption, "#{name} is a table because it has #{references_from.size} references to it"
36
+ @is_table = true
37
+ else
38
+ @is_table = false
39
+ end
40
+ @tentative = false
41
+
42
+ @is_table
43
+ end
44
+
45
+ # REVISIT: Find a better way to determine AutoCounters (ValueType unary role?)
46
+ def is_auto_assigned
47
+ type = self;
48
+ type = type.supertype while type.supertype
49
+ type.name =~ /^Auto/
50
+ end
51
+ end
52
+
53
+ class EntityType
54
+ attr_accessor :absorbed_via # A reference from an entity type that fully absorbs this one
55
+
56
+ def is_auto_assigned; false; end
57
+
58
+ # Decide whether this object is currently considered a table or not:
59
+ def is_table
60
+ return @is_table if @is_table != nil # We already make a guess or decision
61
+
62
+ @tentative = false
63
+
64
+ # Always a table if marked so
65
+ if is_independent
66
+ debug :absorption, "EntityType #{name} is declared independent"
67
+ return @is_table = true
68
+ end
69
+
70
+ # Always a table if nowhere else to go, and has no one-to-ones that might flip:
71
+ if references_to.empty? and
72
+ !references_from.detect{|ref| ref.role_type == :one_one }
73
+ debug :absorption, "EntityType #{name} is independent as it has nowhere to go"
74
+ return @is_table = true
75
+ end
76
+
77
+ # Subtypes are not a table unless partitioned or separate
78
+ # REVISIT: Support partitioned subtypes here
79
+ if (!supertypes.empty?)
80
+ av = all_supertype_inheritance[0]
81
+ return @is_table = false
82
+ end
83
+
84
+ # If the preferred_identifier includes an auto_assigned ValueType
85
+ # and this object is absorbed in more than one place, we need a table
86
+ # to manage the auto-assignment.
87
+ if references_to.size > 1 and
88
+ preferred_identifier.role_sequence.all_role_ref.detect {|rr|
89
+ next false unless rr.role.concept.is_a? ValueType
90
+ rr.role.concept.is_auto_assigned
91
+ }
92
+ debug :absorption, "#{name} has an auto-assigned counter in its ID, so must be a table"
93
+ @tentative = false
94
+ return @is_table = true
95
+ end
96
+
97
+ @tentative = true
98
+ @is_table = true
99
+ end
100
+ end # EntityType class
101
+
102
+ class Role
103
+ def role_type
104
+ # TypeInheritance roles are always 1:1
105
+ if TypeInheritance === fact_type
106
+ return concept == fact_type.supertype ? :supertype : :subtype
107
+ end
108
+
109
+ # Always N:1 if unary:
110
+ return :unary if fact_type.all_role.size == 1
111
+
112
+ # List the UCs on this fact type:
113
+ all_uniqueness_constraints =
114
+ fact_type.all_role.map do |fact_role|
115
+ fact_role.all_role_ref.map do |rr|
116
+ rr.role_sequence.all_presence_constraint.select do |pc|
117
+ pc.max_frequency == 1
118
+ end
119
+ end
120
+ end.flatten.uniq
121
+
122
+ to_1 =
123
+ all_uniqueness_constraints.
124
+ detect do |c|
125
+ c.role_sequence.all_role_ref.size == 1 and
126
+ c.role_sequence.all_role_ref[0].role == self
127
+ end
128
+
129
+ if fact_type.entity_type
130
+ # This is a role in an objectified fact type
131
+ from_1 = true
132
+ else
133
+ # It's to-1 if a UC exists over roles of this FT that doesn't cover this role:
134
+ from_1 = all_uniqueness_constraints.detect{|uc|
135
+ !uc.role_sequence.all_role_ref.detect{|rr| rr.role == self || rr.role.fact_type != fact_type}
136
+ }
137
+ end
138
+
139
+ if from_1
140
+ return to_1 ? :one_one : :one_many
141
+ else
142
+ return to_1 ? :many_one : :many_many
143
+ end
144
+ end
145
+
146
+ end
147
+
148
+ class Vocabulary
149
+ # return an Array of Concepts that will have their own tables
150
+ def tables
151
+ decide_tables if !@tables
152
+ @tables
153
+ end
154
+
155
+ def decide_tables
156
+ # Strategy:
157
+ # 1) Populate references for all Concepts
158
+ # 2) Decide which Concepts must be and must not be tables
159
+ # a. Concepts labelled is_independent are tables (See the is_table methods above)
160
+ # b. Entity types having no references to them must be tables
161
+ # c. subtypes are not tables unless marked is_independent (separate) or partitioned (not yet impl)
162
+ # d. ValueTypes are never tables unless they can have references (to other ValueTypes)
163
+ # e. An EntityType having an identifying AutoInc field must be a table unless it has exactly one reference
164
+ # f. An EntityType whose only reference is through its single preferred_identifier role gets absorbed
165
+ # g. An EntityType that must has references other than its PI must be a table (unless it has exactly one reference to it)
166
+ # h. supertypes are elided if all roles are absorbed into subtypes:
167
+ # - partitioned subtype exhaustion
168
+ # - subtype extension where supertype has only PI roles and no AutoInc
169
+ # 3) any ValueType that has references from it must become a table if not already
170
+
171
+ populate_all_references
172
+
173
+ debug :absorption, "Calculating relational composition" do
174
+ # Evaluate the possible independence of each concept, building an array of features of indeterminate status:
175
+ undecided =
176
+ all_feature.select do |feature|
177
+ next unless feature.is_a? Concept
178
+ feature.is_table # Ask it whether it thinks it should be a table
179
+ feature.tentative # Selection criterion
180
+ end
181
+
182
+ if debug :absorption, "Generating tables, #{undecided.size} undecided"
183
+ (all_feature-undecided).each {|feature|
184
+ next if ValueType === feature && !feature.is_table # Skip unremarkable cases
185
+ debug :absorption do
186
+ debug :absorption, "#{feature.name} is #{feature.is_table ? "" : "not "}a table#{feature.tentative ? ", tentatively" : ""}"
187
+ end
188
+ }
189
+ end
190
+
191
+ pass = 0
192
+ begin # Loop while we continue to make progress
193
+ pass += 1
194
+ debug :absorption, "Starting composition pass #{pass} with #{undecided.size} undecided tables"
195
+ possible_flips = {} # A hash by table containing an array of references that can be flipped
196
+ finalised = # Make an array of things we finalised during this pass
197
+ undecided.select do |feature|
198
+ debug :absorption, "Considering #{feature.name}:" do
199
+ debug :absorption, "refs to #{feature.name} are from #{feature.references_to.map{|ref| ref.from.name}*", "}" if feature.references_to.size > 0
200
+ debug :absorption, "refs from #{feature.name} are to #{feature.references_from.map{|ref| ref.to.name rescue ref.fact_type.default_reading}*", "}" if feature.references_from.size > 0
201
+
202
+ # Always absorb an objectified unary into its role player:
203
+ if feature.fact_type && feature.fact_type.all_role.size == 1
204
+ debug :absorption, "Absorb objectified unary #{feature.name} into #{feature.fact_type.entity_type.name}"
205
+ feature.definitely_not_table
206
+ next feature
207
+ end
208
+
209
+ # If the PI contains one role only, played by an entity type that can absorb us, do that.
210
+ pi_roles = feature.preferred_identifier.role_sequence.all_role_ref.map(&:role)
211
+ debug :absorption, "pi_roles are played by #{pi_roles.map{|role| role.concept.name}*", "}"
212
+ first_pi_role = pi_roles[0]
213
+ pi_ref = nil
214
+ if pi_roles.size == 1 and
215
+ feature.references_to.detect{|ref| pi_ref = ref if ref.from_role == first_pi_role && ref.from.is_a?(EntityType)}
216
+
217
+ debug :absorption, "#{feature.name} is fully absorbed along its sole reference path into entity type #{pi_ref.from.name}"
218
+ feature.definitely_not_table
219
+ next feature
220
+ end
221
+
222
+ # If there's more than one absorption path and any functional dependencies that can't absorb us, it's a table
223
+ non_identifying_refs_from =
224
+ feature.references_from.reject{|ref|
225
+ pi_roles.include?(ref.to_role)
226
+ }
227
+ debug :absorption, "#{feature.name} has #{non_identifying_refs_from.size} non-identifying functional roles"
228
+
229
+ if feature.references_to.size > 1 and
230
+ non_identifying_refs_from.size > 0
231
+ debug :absorption, "#{feature.name} has non-identifying functional dependencies so 3NF requires it be a table"
232
+ feature.definitely_table
233
+ next feature
234
+ end
235
+
236
+ absorption_paths =
237
+ (
238
+ non_identifying_refs_from.reject do |ref|
239
+ !ref.to or ref.to.absorbed_via == ref
240
+ end+feature.references_to
241
+ ).reject do |ref|
242
+ next true if !ref.to.is_table or
243
+ ![:one_one, :supertype, :subtype].include?(ref.role_type)
244
+
245
+ # If one side is mandatory but not the other, don't absorb the mandatory side into the non-mandatory one
246
+ from_is_mandatory = !!ref.is_mandatory
247
+ to_is_mandatory = !ref.to_role || !!ref.to_role.is_mandatory
248
+ bad = (to_is_mandatory != from_is_mandatory and (ref.from == feature ? from_is_mandatory : to_is_mandatory))
249
+ debug :absorption, "Not absorbing mandatory #{feature.name} through #{ref}" if bad
250
+ bad
251
+ end
252
+
253
+ # If this object can be fully absorbed, do that (might require flipping some references)
254
+ if absorption_paths.size > 0
255
+ debug :absorption, "#{feature.name} is fully absorbed through #{absorption_paths.inspect}"
256
+ absorption_paths.each do |ref|
257
+ debug :absorption, "flip #{ref} so #{feature.name} can be absorbed"
258
+ ref.flip if feature == ref.from
259
+ end
260
+ feature.definitely_not_table
261
+ next feature
262
+ end
263
+
264
+ if non_identifying_refs_from.size == 0
265
+ # and (!feature.is_a?(EntityType) ||
266
+ # # REVISIT: The roles may be collectively but not individually mandatory.
267
+ # feature.references_to.detect { |ref| !ref.from_role || ref.from_role.is_mandatory })
268
+ debug :absorption, "#{feature.name} is fully absorbed in #{feature.references_to.size} places: #{feature.references_to.map{|ref| ref.from.name}*", "}"
269
+ feature.definitely_not_table
270
+ next feature
271
+ end
272
+
273
+ false # Failed to decide about this entity_type this time around
274
+ end
275
+ end
276
+
277
+ undecided -= finalised
278
+ debug :absorption, "Finalised #{finalised.size} this pass: #{finalised.map{|f| f.name}*", "}"
279
+ end while !finalised.empty?
280
+
281
+ # A ValueType that isn't explicitly a table and isn't needed anywhere doesn't matter,
282
+ # unless it should absorb something else (another ValueType is all it could be):
283
+ all_feature.each do |feature|
284
+ if (!feature.is_table and feature.references_to.size == 0 and feature.references_from.size > 0)
285
+ debug :absorption, "Making #{feature.name} a table; it has nowhere else to go and needs to absorb things"
286
+ feature.probably_table
287
+ end
288
+ end
289
+
290
+ # Now, evaluate all possibilities of the tentative assignments
291
+ # Incomplete. Apparently unnecessary as well... so far. We'll see.
292
+ if debug :absorption
293
+ undecided.each do |feature|
294
+ debug :absorption, "Unable to decide independence of #{feature.name}, going with #{feature.show_tabular}"
295
+ end
296
+ end
297
+ end
298
+
299
+ populate_all_columns
300
+ populate_all_indices
301
+
302
+ @tables = all_feature.select { |f| f.is_table }
303
+ end
304
+ end
305
+
306
+ end
307
+ end
@@ -8,7 +8,7 @@
8
8
  $debug_indent = 0
9
9
  $debug_keys = {}
10
10
  if (e = ENV["DEBUG"])
11
- e.split(/[^a-z]/).each{|k| $debug_keys[k.to_sym] = true }
11
+ e.split(/[^a-zA-Z0-9]/).each{|k| $debug_keys[k.to_sym] = true }
12
12
  end
13
13
  end
14
14
 
@@ -1,3 +1,3 @@
1
1
  module ActiveFacts
2
- VERSION = '0.6.0'
2
+ VERSION = '0.7.0'
3
3
  end
@@ -40,6 +40,16 @@ module ActiveFacts
40
40
  }
41
41
  end
42
42
 
43
+ def is_mandatory
44
+ all_role_ref.detect{|rr|
45
+ rs = rr.role_sequence
46
+ rs.all_role_ref.size == 1 and
47
+ rs.all_presence_constraint.detect{|pc|
48
+ pc.min_frequency and pc.min_frequency >= 1 and pc.is_mandatory
49
+ }
50
+ }
51
+ end
52
+
43
53
  # Return the RoleRef to this role from its fact type's preferred_reading
44
54
  def preferred_reference
45
55
  fact_type.preferred_reading.role_sequence.all_role_ref.detect{|rr| rr.role == self }
@@ -73,6 +83,19 @@ module ActiveFacts
73
83
  end
74
84
  return joiner ? Array(name_array)*joiner : Array(name_array)
75
85
  end
86
+
87
+ # Two RoleRefs are equal if they have the same role and JoinPaths with matching roles
88
+ def ==(role_ref)
89
+ RoleRef === role_ref &&
90
+ role_ref.role == role &&
91
+ all_join_path.size == role_ref.all_join_path.size &&
92
+ !all_join_path.sort_by{|j|j.join_step}.
93
+ zip(role_ref.all_join_path.sort_by{|j|j.join_step}).
94
+ detect{|j1,j2|
95
+ j1.input_role != j2.input_role ||
96
+ j1.output_role != j2.output_role
97
+ }
98
+ end
76
99
  end
77
100
 
78
101
  class RoleSequence
@@ -107,9 +130,23 @@ module ActiveFacts
107
130
  role_sequence = rr.role_sequence
108
131
 
109
132
  # The role sequence is only interesting if it cover only this fact's roles
133
+ # or roles of the objectification
110
134
  next if role_sequence.all_role_ref.size < fact_roles.size-1 # Not enough roles
111
135
  next if role_sequence.all_role_ref.size > fact_roles.size # Too many roles
112
- next if role_sequence.all_role_ref.detect{|rsr| ft = rsr.role.fact_type; ft != fact_type }
136
+ next if role_sequence.all_role_ref.detect do |rsr|
137
+ if (of = rsr.role.fact_type) != fact_type
138
+ case of.all_role.size
139
+ when 1 # A unary FT must be played by the objectification of this fact type
140
+ next rsr.role.concept != fact_type.entity_type
141
+ when 2 # A binary FT must have the objectification of this FT as the other player
142
+ other_role = (of.all_role-[rsr.role])[0]
143
+ next other_role.concept != fact_type.entity_type
144
+ else
145
+ next true # A role in a ternary (or higher) cannot be usd in our identifier
146
+ end
147
+ end
148
+ rsr.role.fact_type != fact_type
149
+ end
113
150
 
114
151
  # This role sequence is a candidate
115
152
  pc = role_sequence.all_presence_constraint.detect{|c|
@@ -243,10 +280,10 @@ module ActiveFacts
243
280
 
244
281
  # An array of self followed by all supertypes in order:
245
282
  def supertypes_transitive
246
- ([self] + all_type_inheritance_by_subtype.map{|ti|
247
- # debug ti.class.roles.verbalise; exit
248
- ti.supertype.supertypes_transitive
249
- }).flatten.uniq
283
+ ([self] + all_type_inheritance_by_subtype.map{|ti|
284
+ # debug ti.class.roles.verbalise; exit
285
+ ti.supertype.supertypes_transitive
286
+ }).flatten.uniq
250
287
  end
251
288
 
252
289
  # A subtype does not have a identifying_supertype if it defines its own identifier
@@ -43,7 +43,7 @@ describe "Absorption" do
43
43
  #{Prologue}
44
44
  Month is in exactly one Season;
45
45
  },
46
- :tables => { "Month" => [ %w{Month Value}, "Season" ] }
46
+ :tables => { "Month" => [ "MonthValue", "Season" ] }
47
47
  },
48
48
 
49
49
  { :should => "absorb a one-to-one along the identification path",
@@ -64,7 +64,7 @@ describe "Absorption" do
64
64
  },
65
65
  :tables => {
66
66
  "Claim" => ["ClaimID", %w{Lodgement DateTime}, %w{Lodgement Person ID}],
67
- "Party" => ["PartyID", %w{Person birth Date}]
67
+ "Party" => ["PartyID", %w{Person Birth Date}]
68
68
  }
69
69
  },
70
70
 
@@ -81,13 +81,15 @@ describe "Absorption" do
81
81
  @compiler = ActiveFacts::Input::CQL.new(cql, should)
82
82
  @vocabulary = @compiler.read
83
83
 
84
+ # puts cql
85
+
84
86
  # Ensure that the same tables were generated:
85
- tables = @vocabulary.tables
86
- tables.map(&:name).sort.should == expected_tables.keys.sort
87
+ tables = @vocabulary.tables.sort_by(&:name)
88
+ tables.map(&:name).should == expected_tables.keys.sort
87
89
 
88
90
  # Ensure that the same column descriptions were generated:
89
- tables.sort_by(&:name).each do |table|
90
- column_descriptions = table.absorbed_roles.all_role_ref.map{|rr| rr.column_name(nil) }.sort
91
+ tables.each do |table|
92
+ column_descriptions = table.columns.map{|col| col.name(nil) }.sort
91
93
  column_descriptions.should == expected_tables[table.name].map{|c| Array(c) }.sort
92
94
  end
93
95
  end
data/spec/cql_cql_spec.rb CHANGED
@@ -23,6 +23,7 @@ describe "CQL Loader" do
23
23
  Airline
24
24
  CompanyQuery
25
25
  Insurance
26
+ OddIdentifier
26
27
  OrienteeringER
27
28
  ServiceDirector
28
29
  }
data/spec/cql_sql_spec.rb CHANGED
@@ -17,6 +17,7 @@ describe "CQL Loader with SQL output" do
17
17
  Airline
18
18
  CompanyQuery
19
19
  Insurance
20
+ Marriage
20
21
  OrienteeringER
21
22
  ServiceDirector
22
23
  SimplestUnary
@@ -25,7 +26,7 @@ describe "CQL Loader with SQL output" do
25
26
  # Generate and return the SQL for the given vocabulary
26
27
  def sql(vocabulary)
27
28
  output = StringIO.new
28
- @dumper = ActiveFacts::Generate::SQL::SERVER.new(vocabulary.constellation)
29
+ @dumper = ActiveFacts::Generate::SQL::SERVER.new(vocabulary.constellation, "norma")
29
30
  @dumper.generate(output)
30
31
  output.rewind
31
32
  output.read