activefacts 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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