activefacts 0.7.1 → 0.7.2

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.
@@ -59,7 +59,7 @@ module ActiveFacts
59
59
  # A Column name is a sequence of names (derived from the to_roles of the References)
60
60
  # joined by a joiner string (pass nil to get the original array of names)
61
61
  def name(joiner = "")
62
- last_name = ""
62
+ last_names = []
63
63
  names = @references.
64
64
  reject do |ref|
65
65
  # Skip any object after the first which is identified by this reference
@@ -67,37 +67,41 @@ module ActiveFacts
67
67
  !ref.fact_type.is_a?(TypeInheritance) and
68
68
  ref.to and
69
69
  ref.to.is_a?(EntityType) and
70
- (role_refs = ref.to.preferred_identifier.role_sequence.all_role_ref).size == 1 and
71
- role_refs.only.role == ref.from_role
70
+ (role_ref = ref.to.preferred_identifier.role_sequence.all_role_ref.single) and
71
+ role_ref.role == ref.from_role
72
72
  end.
73
73
  inject([]) do |a, ref|
74
74
  names = ref.to_names
75
75
 
76
76
  # When traversing type inheritances, keep the subtype name, not the supertype names as well:
77
77
  if a.size > 0 && ref.fact_type.is_a?(TypeInheritance)
78
- a[-1] = names[0] if ref.to == ref.fact_type.subtype # Else we already had the subtype
79
- next a
78
+ next a if ref.to != ref.fact_type.subtype # Did we already have the subtype?
79
+ last_names.size.times { a.pop } # Remove the last names added
80
+ elsif last_names.last && last_names.last == names[0][0...last_names.last.size]
81
+ # When Xyz is followed by XyzID, truncate that to just ID
82
+ names[0] = names[0][last_names.last.size..-1]
83
+ elsif last_names.last == names[0]
84
+ # Same, but where an underscore split up the words
85
+ names.shift
80
86
  end
81
87
 
82
- # When Xyz is followed by XyzID, truncate that to just ID:
83
- names[0] = names[0][last_name.size..-1] if last_name == names[0][0...last_name.size]
84
- last_name = names.last
88
+ # Where the last name is like a reference mode but the preceeding name isn't the identified concept,
89
+ # strip it down (so turn Driver.PartyID into Driver.ID for example):
90
+ if a.size > 0 and
91
+ (et = ref.from).is_a?(EntityType) and
92
+ (role_ref = et.preferred_identifier.role_sequence.all_role_ref.single) and
93
+ role_ref.role == ref.to_role and
94
+ names[0][0...et.name.size].downcase == et.name.downcase
95
+ names[0] = names[0][et.name.size..-1]
96
+ names.shift if names[0] == ""
97
+ end
98
+
99
+ last_names = names
85
100
 
86
101
  a += names
87
102
  a
88
103
  end
89
104
 
90
- # Where the last name is like a reference mode but the preceeding name isn't the identified concept,
91
- # strip it down (so turn Driver.PartyID into Driver.ID for example):
92
- if names.size > 1 and
93
- (et = @references.last.from).is_a?(EntityType) and
94
- (role_refs = et.preferred_identifier.role_sequence.all_role_ref).size == 1 and
95
- role_refs.only.role == @references.last.to_role and
96
- names.last[0...et.name.size].downcase == et.name.downcase
97
- names[-1] = names.last[et.name.size..-1]
98
- names.pop if names.last == ''
99
- end
100
-
101
105
  name_array = names.map{|n| n.sub(/^[a-z]/){|s| s.upcase}}
102
106
  joiner ? name_array * joiner : name_array
103
107
  end
@@ -185,10 +189,13 @@ module ActiveFacts
185
189
  # This section shows the features relevant to relational Persistence.
186
190
  class Concept
187
191
  # The array of columns for this Concept's table
188
- def columns; @columns; end
192
+ def columns
193
+ @columns
194
+ end
189
195
 
190
196
  def populate_columns #:nodoc:
191
- @columns = all_columns({})
197
+ @columns =
198
+ all_columns({})
192
199
  end
193
200
  end
194
201
 
@@ -0,0 +1,73 @@
1
+ module ActiveFacts
2
+ module API
3
+ module Concept
4
+ def table
5
+ @is_table = true
6
+ end
7
+
8
+ def is_table
9
+ @is_table
10
+ end
11
+
12
+ def columns
13
+ #puts "Calculating columns for #{basename}"
14
+ return @columns if @columns
15
+ @columns = (
16
+ roles.
17
+ values.
18
+ select{|role| role.unique}.
19
+ inject([]) do |columns, role|
20
+ rn = role.name.to_s.split(/_/)
21
+ columns += role.counterpart_concept.__absorb(rn, role.counterpart)
22
+ end +
23
+ # REVISIT: Need to use subtypes_transitive here:
24
+ subtypes.
25
+ select{|subtype| !subtype.is_table}. # Don't absorb separate subtypes
26
+ inject([]) { |columns, subtype|
27
+ sn = [subtype.basename]
28
+ columns += subtype.__absorb(sn)
29
+ # puts "subtype #{subtype.name} contributed #{columns.inspect}"
30
+ columns
31
+ }
32
+ ).map{|col_names| col_names.uniq.map{|name| name.sub(/^[a-z]/){|c| c.upcase}}*"."}
33
+ end
34
+
35
+ # Return an array of the absorbed columns, using prefix for name truncation
36
+ def __absorb(prefix, except_role = nil)
37
+ is_entity = respond_to?(:identifying_role_names)
38
+ absorbed_into = nil
39
+ if absorbed_into
40
+ # REVISIT: if this concept is fully absorbed through one of its roles into another table, we absorb that tables identifying_roles
41
+ absorbed_into.__absorb(prefix)
42
+ elsif !@is_table
43
+ # Not a table -> all roles are absorbed
44
+ if is_entity
45
+ roles.
46
+ values.
47
+ select{|role| role.unique && role.counterpart_concept != except_role }.
48
+ inject([]) do |columns, role|
49
+ columns += role.counterpart_concept.__absorb(prefix + role.name.to_s.split(/_/), self)
50
+ end
51
+ else
52
+ [prefix]
53
+ end
54
+ else
55
+ #puts "#{@is_table ? "referencing" : "absorbing"} #{is_entity ? "entity" : "value"} #{basename} using #{prefix.inspect}"
56
+ if is_entity
57
+ identifying_role_names.map{|role_name| prefix+role_name.to_s.split(/_/)}
58
+ else
59
+ # Reference to value type which is a table
60
+ [prefix + ["Value"]]
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ end
67
+ end
68
+
69
+ class TrueClass
70
+ def self.__absorb(prefix, except_role = nil)
71
+ [prefix]
72
+ end
73
+ end
@@ -35,7 +35,11 @@ module ActiveFacts
35
35
  # Return an Array of Reference paths for such absorbed FKs
36
36
  def all_absorbed_foreign_key_reference_path
37
37
  references_from.inject([]) do |array, ref|
38
+
38
39
  if ref.is_simple_reference
40
+ # This catches references that would be created to secondary supertypes, when absorption is through primary.
41
+ # There might be other cases where an exclusion like this is needed, but I can't reason it out.
42
+ next array if TypeInheritance === ref.fact_type && absorbed_via && TypeInheritance === absorbed_via.fact_type
39
43
  array << [ref]
40
44
  elsif ref.is_absorbing
41
45
  ref.to.all_absorbed_foreign_key_reference_path.each{|aref|
@@ -102,8 +106,20 @@ module ActiveFacts
102
106
  end
103
107
  debug :fk, "to_columns in #{to.name}: #{to_columns.map { |column| column ? column.name : "OOPS!" }*", "}"
104
108
 
105
- ActiveFacts::Persistence::ForeignKey.new(self, to, fk_ref_path[-1], from_columns, to_columns)
109
+ # Put the column pairs in a defined order, sorting key pairs by to-name:
110
+ froms, tos = from_columns.zip(to_columns).sort_by { |pair|
111
+ pair[1].name(nil)
112
+ }.transpose
113
+
114
+ ActiveFacts::Persistence::ForeignKey.new(self, to, fk_ref_path[-1], froms, tos)
106
115
  end
116
+ end.
117
+ sort_by do |fk|
118
+ # Put the foreign keys in a defined order:
119
+ [ fk.to.name,
120
+ fk.to_columns.map{|col| col.name(nil).sort},
121
+ fk.from_columns.map{|col| col.name(nil).sort}
122
+ ]
107
123
  end
108
124
  end
109
125
  end
@@ -94,10 +94,14 @@ module ActiveFacts
94
94
  # The reference path is the set of absorption references and one past it.
95
95
  # Stopping here means we don't dig into the definitions of FK column counterparts.
96
96
  # Note that many columns of an object may have the same ref_path.
97
+ #
98
+ # REVISIT:
99
+ # Note also that this produces columns ordered for each refpath the same as the
100
+ # order of the columns, not the same as the columns in the PK for which they might be an FK.
97
101
  all_column_by_ref_path =
98
102
  debug :index2, "Indexing columns by ref_path" do
99
- columns.inject({}) do |hash, column|
100
- debug :index2, "References in column #{name}#{column.name}" do
103
+ @columns.inject({}) do |hash, column|
104
+ debug :index2, "References in column #{name}.#{column.name}" do
101
105
  ref_path = column.absorption_references
102
106
  raise "No absorption_references for #{column.name} from #{column.references.map(&:to_s)*" and "}" if !ref_path || ref_path.empty?
103
107
  (hash[ref_path] ||= []) << column
@@ -114,15 +118,17 @@ module ActiveFacts
114
118
  inject({}) do |hash, ref_path|
115
119
  ref_path.each do |ref|
116
120
  next unless ref.to_role
121
+ #debug :index2, "Considering #{ref_path.map(&:to_s)*" and "} yielding columns #{all_column_by_ref_path[ref_path].map{|c| c.name(".")}*", "}"
122
+ #debugger if name == 'VehicleIncident' && ref.fact_type.is_a?(ActiveFacts::Metamodel::TypeInheritance)
117
123
  ref.to_role.all_role_ref.each do |role_ref|
118
- pcs = role_ref.role_sequence.all_presence_constraint.
124
+ all_pcs = role_ref.role_sequence.all_presence_constraint
125
+ #puts "pcs over #{ref_path.map{|r| r.to_names}.flatten*"."}: #{role_ref.role_sequence.all_presence_constraint.map(&:describe)*"; "}" if all_pcs.size > 0
126
+ pcs = all_pcs.
119
127
  reject do |pc|
120
128
  !pc.max_frequency or # No maximum freq; cannot be a uniqueness constraint
121
129
  pc.max_frequency != 1 or # maximum is not 1
122
- pc.role_sequence.all_role_ref.size == 1 && # UniquenessConstraint is over one role
123
- ((fact_type = pc.role_sequence.all_role_ref.only.role.fact_type).is_a?(TypeInheritance) || # Inheritance
124
- fact_type.all_role.size == 1) # Unary
125
- # The preceeeding two restrictions exclude the internal UCs created within NORMA.
130
+ # Constraint is not over a unary fact type role (NORMA does this)
131
+ pc.role_sequence.all_role_ref.size == 1 && ref_path[-1].to_role.fact_type.all_role.size == 1
126
132
  end
127
133
  next unless pcs.size > 0
128
134
  # The columns for this ref_path support the UCs in "pcs".
@@ -151,8 +157,8 @@ module ActiveFacts
151
157
  # Absorption through a one-to-one forms a UC that we don't need to enforce using an index:
152
158
  next nil if over != self and
153
159
  over.absorbed_via == columns[0].references[absorption_level-1] and
154
- (rrs = uc.role_sequence.all_role_ref).size == 1 and
155
- over.absorbed_via.fact_type.all_role.include?(rrs.only.role)
160
+ (rr = uc.role_sequence.all_role_ref.single) and
161
+ over.absorbed_via.fact_type.all_role.include?(rr.role)
156
162
 
157
163
  index = ActiveFacts::Persistence::Index.new(
158
164
  uc,
@@ -163,7 +169,12 @@ module ActiveFacts
163
169
  )
164
170
  debug :index, index
165
171
  index
166
- end.compact
172
+ end.
173
+ compact.
174
+ sort_by do |index|
175
+ # Put the indices in a defined order:
176
+ index.columns.map(&:name)
177
+ end
167
178
  end
168
179
  end
169
180
 
@@ -114,16 +114,16 @@ module ActiveFacts
114
114
  def to_names
115
115
  case
116
116
  when is_unary
117
- @to_role.fact_type.preferred_reading.reading_text.gsub(/\{[0-9]\}/,'').strip.split(/\s/)
117
+ @to_role.fact_type.preferred_reading.reading_text.gsub(/\{[0-9]\}/,'').strip.split(/[_\s]/)
118
118
  when @to && !@to_role # @to is an objectified fact type so @to_role is a phantom
119
- [@to.name]
119
+ @to.name.split(/[_\s]/)
120
120
  when !@to_role # Self-value role of an independent ValueType
121
- ["#{@from.name}Value"]
121
+ ["#{@from.name}", "Value"]
122
122
  when @to_role.role_name # Named role
123
- [@to_role.role_name]
123
+ @to_role.role_name.split(/[_\s]/)
124
124
  else # Use the name from the preferred reading
125
125
  role_ref = @to_role.preferred_reference
126
- [role_ref.leading_adjective, @to_role.concept.name, role_ref.trailing_adjective].compact.map{|w| w.split(/\s/)}.flatten.reject{|s| s == ''}
126
+ [role_ref.leading_adjective, @to_role.concept.name, role_ref.trailing_adjective].compact.map{|w| w.split(/[_\s]/)}.flatten.reject{|s| s == ''}
127
127
  end
128
128
  end
129
129
 
@@ -315,7 +315,7 @@ module ActiveFacts
315
315
  end
316
316
  end
317
317
 
318
- class EntityType
318
+ class EntityType < Concept
319
319
  def populate_references #:nodoc:
320
320
  if fact_type && fact_type.all_role.size > 1
321
321
  # NOT: fact_type.all_role.each do |role| # Place roles in the preferred order instead:
@@ -17,7 +17,7 @@ require 'activefacts/persistence/reference'
17
17
  module ActiveFacts
18
18
  module Metamodel
19
19
 
20
- class ValueType
20
+ class ValueType < Concept
21
21
  def absorbed_via #:nodoc:
22
22
  # ValueTypes aren't absorbed in the way EntityTypes are
23
23
  nil
@@ -55,7 +55,7 @@ module ActiveFacts
55
55
  end
56
56
  end
57
57
 
58
- class EntityType
58
+ class EntityType < Concept
59
59
  # A Reference from an entity type that fully absorbs this one
60
60
  def absorbed_via; @absorbed_via; end
61
61
  def absorbed_via=(r) #:nodoc:
@@ -133,8 +133,8 @@ module ActiveFacts
133
133
  to_1 =
134
134
  all_uniqueness_constraints.
135
135
  detect do |c|
136
- c.role_sequence.all_role_ref.size == 1 and
137
- c.role_sequence.all_role_ref.only.role == self
136
+ (rr = c.role_sequence.all_role_ref.single) and
137
+ rr.role == self
138
138
  end
139
139
 
140
140
  if fact_type.entity_type
@@ -310,7 +310,10 @@ module ActiveFacts
310
310
  populate_all_columns
311
311
  populate_all_indices
312
312
 
313
- @tables = all_feature.select { |f| f.is_table }
313
+ @tables =
314
+ all_feature.
315
+ select { |f| f.is_table }.
316
+ sort_by { |table| table.name }
314
317
  end
315
318
  end
316
319
 
@@ -5,5 +5,5 @@
5
5
  # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
6
6
  #
7
7
  module ActiveFacts
8
- VERSION = '0.7.1'
8
+ VERSION = '0.7.2'
9
9
  end
@@ -58,7 +58,7 @@ module ActiveFacts
58
58
  end
59
59
  end
60
60
 
61
- class JoinPath
61
+ class Join
62
62
  def column_name(joiner = '-')
63
63
  concept == input_role.concept ? input_role.preferred_reference.role_name(joiner) : Array(concept.name)
64
64
  end
@@ -72,8 +72,8 @@ module ActiveFacts
72
72
 
73
73
  class RoleRef
74
74
  def describe
75
- # The reference traverses the JoinPaths in sequence to the final role:
76
- all_join_path.sort_by{|jp| jp.join_step}.map{ |jp| jp.describe + "." }*"" + role_name
75
+ # The reference traverses the Joins in sequence to the final role:
76
+ all_join.sort_by{|jp| jp.join_step}.map{ |jp| jp.describe + "." }*"" + role_name
77
77
  end
78
78
 
79
79
  def role_name(joiner = "-")
@@ -86,13 +86,13 @@ module ActiveFacts
86
86
  return joiner ? Array(name_array)*joiner : Array(name_array)
87
87
  end
88
88
 
89
- # Two RoleRefs are equal if they have the same role and JoinPaths with matching roles
89
+ # Two RoleRefs are equal if they have the same role and Joins with matching roles
90
90
  def ==(role_ref)
91
91
  RoleRef === role_ref &&
92
92
  role_ref.role == role &&
93
- all_join_path.size == role_ref.all_join_path.size &&
94
- !all_join_path.sort_by{|j|j.join_step}.
95
- zip(role_ref.all_join_path.sort_by{|j|j.join_step}).
93
+ all_join.size == role_ref.all_join.size &&
94
+ !all_join.sort_by{|j|j.join_step}.
95
+ zip(role_ref.all_join.sort_by{|j|j.join_step}).
96
96
  detect{|j1,j2|
97
97
  j1.input_role != j2.input_role ||
98
98
  j1.output_role != j2.output_role
@@ -376,6 +376,16 @@ module ActiveFacts
376
376
  ((max && min == max) ? "#{max == 1 ? "one" : max.to_s}" : nil)
377
377
  ].compact * " and"
378
378
  end
379
+
380
+ def describe
381
+ role_sequence.describe + " occurs " + frequency + " time"
382
+ end
383
+ end
384
+
385
+ class TypeInheritance
386
+ def describe(role = nil)
387
+ "#{subtype.name} is a kind of #{supertype.name}"
388
+ end
379
389
  end
380
390
 
381
391
  end
@@ -55,6 +55,11 @@ module ActiveFacts
55
55
  value_type :length => 32
56
56
  end
57
57
 
58
+ class Pronoun < String
59
+ value_type
60
+ # REVISIT: Pronoun has restricted values
61
+ end
62
+
58
63
  class ReadingText < String
59
64
  value_type :length => 256
60
65
  end
@@ -90,7 +95,7 @@ module ActiveFacts
90
95
  end
91
96
 
92
97
  class Coefficient
93
- identified_by :numerator, :denominator
98
+ identified_by :numerator, :denominator, :is_precise
94
99
  has_one :denominator # See Denominator.all_coefficient
95
100
  maybe :is_precise
96
101
  has_one :numerator # See Numerator.all_coefficient
@@ -175,11 +180,11 @@ module ActiveFacts
175
180
  one_to_one :unit_id # See UnitId.unit
176
181
  end
177
182
 
178
- class UnitBasis
179
- identified_by :base_unit, :derived_unit
180
- has_one :base_unit, Unit # See Unit.all_unit_basis_as_base_unit
181
- has_one :derived_unit, Unit # See Unit.all_unit_basis_as_derived_unit
182
- has_one :exponent # See Exponent.all_unit_basis
183
+ class Derivation
184
+ identified_by :derived_unit, :base_unit
185
+ has_one :base_unit, Unit # See Unit.all_derivation_as_base_unit
186
+ has_one :derived_unit, Unit # See Unit.all_derivation_as_derived_unit
187
+ has_one :exponent # See Exponent.all_derivation
183
188
  end
184
189
 
185
190
  class ValueRange
@@ -194,7 +199,7 @@ module ActiveFacts
194
199
  end
195
200
 
196
201
  class AllowedRange
197
- identified_by :value_range, :value_restriction
202
+ identified_by :value_restriction, :value_range
198
203
  has_one :value_range # See ValueRange.all_allowed_range
199
204
  has_one :value_restriction # See ValueRestriction.all_allowed_range
200
205
  end
@@ -205,19 +210,19 @@ module ActiveFacts
205
210
  end
206
211
 
207
212
  class Import
208
- identified_by :imported_vocabulary, :vocabulary
213
+ identified_by :vocabulary, :imported_vocabulary
209
214
  has_one :imported_vocabulary, Vocabulary # See Vocabulary.all_import_as_imported_vocabulary
210
215
  has_one :vocabulary # See Vocabulary.all_import
211
216
  end
212
217
 
213
218
  class Feature
214
- identified_by :name, :vocabulary
219
+ identified_by :vocabulary, :name
215
220
  has_one :name # See Name.all_feature
216
221
  has_one :vocabulary # See Vocabulary.all_feature
217
222
  end
218
223
 
219
224
  class Correspondence
220
- identified_by :imported_feature, :import
225
+ identified_by :import, :imported_feature
221
226
  has_one :import # See Import.all_correspondence
222
227
  has_one :imported_feature, Feature # See Feature.all_correspondence_as_imported_feature
223
228
  has_one :local_feature, Feature # See Feature.all_correspondence_as_local_feature
@@ -251,7 +256,7 @@ module ActiveFacts
251
256
 
252
257
  class Concept < Feature
253
258
  maybe :is_independent
254
- maybe :is_personal
259
+ has_one :pronoun # See Pronoun.all_concept
255
260
  end
256
261
 
257
262
  class Role
@@ -272,13 +277,13 @@ module ActiveFacts
272
277
  has_one :trailing_adjective, Adjective # See Adjective.all_role_ref_as_trailing_adjective
273
278
  end
274
279
 
275
- class JoinPath
280
+ class Join
276
281
  identified_by :role_ref, :join_step
277
- has_one :join_step, Ordinal # See Ordinal.all_join_path_as_join_step
278
- has_one :role_ref # See RoleRef.all_join_path
279
- has_one :concept # See Concept.all_join_path
280
- has_one :input_role, Role # See Role.all_join_path_as_input_role
281
- has_one :output_role, Role # See Role.all_join_path_as_output_role
282
+ has_one :join_step, Ordinal # See Ordinal.all_join_as_join_step
283
+ has_one :role_ref # See RoleRef.all_join
284
+ has_one :concept # See Concept.all_join
285
+ has_one :input_role, Role # See Role.all_join_as_input_role
286
+ has_one :output_role, Role # See Role.all_join_as_output_role
282
287
  end
283
288
 
284
289
  class EntityType < Concept