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
@@ -237,9 +237,6 @@ describe "Entity Types" do
237
237
  [ "Employee is a subtype of Person;",
238
238
  [["Employee", [:entity_type, ["Person"], nil, nil]]]
239
239
  ],
240
- [ "Employee is defined as subtype of Person;",
241
- [["Employee", [:entity_type, ["Person"], nil, nil]]]
242
- ],
243
240
  [ "AustralianEmployee is a subtype of Employee, Australian;",
244
241
  [["AustralianEmployee", [:entity_type, ["Employee", "Australian"], nil, nil]]]
245
242
  ],
@@ -249,9 +246,6 @@ describe "Entity Types" do
249
246
  [ "Employee is a subtype of Person identified by EmployeeNumber;",
250
247
  [["Employee", [:entity_type, ["Person"], {:roles=>[["EmployeeNumber"]]}, nil]]]
251
248
  ],
252
- [ "Employee is defined as subtype of Person identified by EmployeeNumber;",
253
- [["Employee", [:entity_type, ["Person"], {:roles=>[["EmployeeNumber"]]}, nil]]]
254
- ],
255
249
  [ "AustralianEmployee is a subtype of Employee, Australian identified by TaxFileNumber;",
256
250
  [["AustralianEmployee", [:entity_type, ["Employee", "Australian"], {:roles=>[["TaxFileNumber"]]}, nil]]]
257
251
  ],
@@ -13,6 +13,7 @@ include ActiveFacts
13
13
 
14
14
  describe "Norma Loader" do
15
15
  ORM_CQL_FAILURES = %w{
16
+ OddIdentifier
16
17
  ServiceDirector
17
18
  }
18
19
  # Generate and return the CQL for the given vocabulary
@@ -17,7 +17,7 @@ describe "NORMA Loader with SQL output" do
17
17
  # Generate and return the SQL for the given vocabulary
18
18
  def sql(vocabulary)
19
19
  output = StringIO.new
20
- @dumper = ActiveFacts::Generate::SQL::SERVER.new(vocabulary.constellation)
20
+ @dumper = ActiveFacts::Generate::SQL::SERVER.new(vocabulary.constellation, "norma")
21
21
  @dumper.generate(output)
22
22
  output.rewind
23
23
  output.read
@@ -19,76 +19,74 @@ include ActiveFacts::Metamodel
19
19
  Exceptions = {
20
20
  "Blog" => ["Author", "Comment", "Paragraph", "Post", "Topic"],
21
21
  "DeathAsBinary" => ["Person"],
22
- "Metamodel" => ["AllowedRange", "Constraint", "Correspondence", "Fact", "FactType", "Feature", "Instance", "JoinPath", "Reading", "Role", "RoleRef", "RoleSequence", "RoleValue", "SetComparisonRoles", "Unit", "UnitBasis", "ValueRestriction"],
22
+ "Metamodel" => ["AllowedRange", "Constraint", "Correspondence", "Derivation", "Fact", "FactType", "Feature", "Instance", "JoinPath", "Reading", "Role", "RoleRef", "RoleSequence", "RoleValue", "SetComparisonRoles", "Unit", "ValueRestriction"],
23
+ "MetamodelTerms" => ["AllowedValue", "Concept", "Constraint", "Derivation", "Fact", "FactType", "Import", "Instance", "Join", "JoinRole", "ParamValue", "Reading", "Role", "RoleRef", "RoleSequence", "RoleValue", "SetComparisonRoles", "Term", "Unit", "ValueRestriction"],
24
+ "OilSupply" => ["AcceptableSubstitutes", "Month", "ProductionForecast", "RegionalDemand", "TransportRoute"],
23
25
  "OilSupplyWithCosts" => ["AcceptableSubstitutes", "Month", "ProductionForecast", "RegionalDemand", "TransportRoute"],
24
26
  "Orienteering" => ["Club", "Entry", "Event", "EventControl", "EventScoringMethod", "Map", "Person", "Punch", "PunchPlacement", "Series", "Visit"],
27
+ "SeparateSubtype" => ["Claim", "VehicleIncident"],
25
28
  "Warehousing" => ["Bin", "DirectOrderMatch", "DispatchItem", "Party", "Product", "PurchaseOrder", "PurchaseOrderItem", "ReceivedItem", "SalesOrder", "SalesOrderItem", "TransferRequest", "Warehouse"]
26
29
  }
27
30
 
31
+ def extract_created_tables_from_sql sql_file
32
+ File.open(sql_file) do |f|
33
+ f.
34
+ readlines.
35
+ select do |l|
36
+ l =~ /CREATE TABLE/
37
+ end.
38
+ map do |l|
39
+ l.chomp.gsub(/.*CREATE TABLE\s+\W*(\w+\.)?"?(\w+)"?.*/, '\2')
40
+ end.
41
+ sort
42
+ end
43
+ end
44
+
28
45
  describe "Relational Composition from NORMA" do
29
46
  #Dir["examples/norma/B*.orm"].each do |orm_file|
30
47
  #Dir["examples/norma/Ins*.orm"].each do |orm_file|
48
+ #Dir["examples/norma/Meta*.orm"].each do |orm_file|
31
49
  #Dir["examples/norma/W*.orm"].each do |orm_file|
32
50
  Dir["examples/norma/*.orm"].each do |orm_file|
33
- sql_tables = Exceptions[File.basename(orm_file, ".orm")]
34
- if !sql_tables
51
+ expected_tables = Exceptions[File.basename(orm_file, ".orm")]
52
+ if !expected_tables
35
53
  sql_file_pattern = orm_file.sub(/\.orm\Z/, '*.sql')
36
- sql_files = Dir[sql_file_pattern]
37
- next unless sql_files.size > 0
54
+ sql_file = Dir[sql_file_pattern][0]
55
+ next unless sql_file
38
56
  end
39
57
 
40
58
  it "should load #{orm_file} and compute #{
41
- sql_tables ? "the expected list of tables" :
42
- "a list of tables similar to those in #{sql_files[0]}"
59
+ expected_tables ?
60
+ "the expected list of tables" :
61
+ "a list of tables similar to those in #{sql_file}"
43
62
  }" do
44
63
 
64
+ # Read the ORM file:
45
65
  vocabulary = ActiveFacts::Input::ORM.readfile(orm_file)
46
66
 
47
67
  # Get the list of tables from NORMA's SQL:
48
- sql_tables ||= File.open(sql_files[0]) do |f|
49
- f.
50
- readlines.
51
- select do |l|
52
- l =~ /CREATE TABLE/
53
- end.
54
- map do |l|
55
- l.chomp.gsub(/.*CREATE TABLE\s+\W*(\w+\.)?"?(\w+)"?.*/, '\2')
56
- end.
57
- sort
58
- end
68
+ expected_tables ||= extract_created_tables_from_sql(sql_file)
59
69
 
60
70
  # Get the list of tables from our composition:
61
- composition = vocabulary.tables.map{|o| o.name }.sort
71
+ tables = vocabulary.tables
72
+ table_names = tables.map{|o| o.name }.sort
62
73
 
63
74
  # Save the actual and expected composition to files
64
- actual_tables = orm_file.sub(%r{examples/norma/(.*).orm\Z}, 'spec/actual/\1.tables')
65
- File.open(actual_tables, "w") { |f| f.puts composition*"\n" }
66
- norma_tables = orm_file.sub(%r{examples/norma/(.*).orm\Z}, 'spec/actual/\1.norma.tables')
67
- File.open(norma_tables, "w") { |f| f.puts sql_tables*"\n" }
75
+ actual_tables_file = orm_file.sub(%r{examples/norma/(.*).orm\Z}, 'spec/actual/\1.tables')
76
+ File.open(actual_tables_file, "w") { |f| f.puts table_names*"\n" }
77
+ expected_tables_file = orm_file.sub(%r{examples/norma/(.*).orm\Z}, 'spec/actual/\1.expected.tables')
78
+ File.open(expected_tables_file, "w") { |f| f.puts expected_tables*"\n" }
68
79
 
69
- # Calculate the columns and column names; REVISIT: check the results
70
- vocabulary.tables.each {|table| table.absorbed_roles }
80
+ # Check that the list matched:
81
+ table_names.should == expected_tables
71
82
 
72
- if false && composition != sql_tables
73
- #puts "="*20 + " reasons " + "="*20
74
- # Show only the reasons for the differences:
75
- #((composition+sql_tables).uniq-(composition&sql_tables)).
76
- # Show the reasons for all entity types:
77
- vocabulary.
78
- all_feature.
79
- select{|f| EntityType === f || f.independent }.
80
- map{|f| f.name}.
81
- sort.
82
- each do |concept_name|
83
- concept = vocabulary.constellation.Feature(concept_name, vocabulary)
84
- puts "#{concept_name}:\n\t#{concept.dependency_reasons*"\n\t"}"
85
- end
83
+ # Calculate the columns and column names; REVISIT: check the results
84
+ tables.each do |table|
85
+ table.columns
86
86
  end
87
87
 
88
- composition.should == sql_tables
89
-
90
- File.delete(actual_tables)
91
- File.delete(norma_tables)
88
+ File.delete(actual_tables_file)
89
+ File.delete(expected_tables_file)
92
90
  end
93
91
  end
94
92
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activefacts
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Clifford Heath
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2008-11-28 00:00:00 +11:00
12
+ date: 2009-01-09 00:00:00 +11:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -90,7 +90,6 @@ files:
90
90
  - lib/activefacts/api/value.rb
91
91
  - lib/activefacts/api/vocabulary.rb
92
92
  - lib/activefacts/cql.rb
93
- - lib/activefacts/cql/Rakefile
94
93
  - lib/activefacts/cql/CQLParser.treetop
95
94
  - lib/activefacts/cql/Concepts.treetop
96
95
  - lib/activefacts/cql/DataTypes.treetop
@@ -98,11 +97,13 @@ files:
98
97
  - lib/activefacts/cql/FactTypes.treetop
99
98
  - lib/activefacts/cql/Language/English.treetop
100
99
  - lib/activefacts/cql/LexicalRules.treetop
100
+ - lib/activefacts/cql/Rakefile
101
101
  - lib/activefacts/cql/parser.rb
102
102
  - lib/activefacts/generate/absorption.rb
103
103
  - lib/activefacts/generate/cql.rb
104
104
  - lib/activefacts/generate/cql/html.rb
105
105
  - lib/activefacts/generate/null.rb
106
+ - lib/activefacts/generate/oo.rb
106
107
  - lib/activefacts/generate/ordered.rb
107
108
  - lib/activefacts/generate/ruby.rb
108
109
  - lib/activefacts/generate/sql/server.rb
@@ -110,7 +111,11 @@ files:
110
111
  - lib/activefacts/input/cql.rb
111
112
  - lib/activefacts/input/orm.rb
112
113
  - lib/activefacts/persistence.rb
113
- - lib/activefacts/persistence/composition.rb
114
+ - lib/activefacts/persistence/columns.rb
115
+ - lib/activefacts/persistence/foreignkey.rb
116
+ - lib/activefacts/persistence/index.rb
117
+ - lib/activefacts/persistence/reference.rb
118
+ - lib/activefacts/persistence/tables.rb
114
119
  - lib/activefacts/support.rb
115
120
  - lib/activefacts/version.rb
116
121
  - lib/activefacts/vocabulary.rb
@@ -1,653 +0,0 @@
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