activefacts 0.8.16 → 0.8.18

Sign up to get free protection for your applications and to get access to all the features.
Files changed (100) hide show
  1. checksums.yaml +15 -0
  2. data/Manifest.txt +10 -4
  3. data/bin/afgen +26 -20
  4. data/bin/cql +1 -1
  5. data/css/orm2.css +89 -9
  6. data/examples/CQL/CompanyDirectorEmployee.cql +4 -4
  7. data/examples/CQL/Genealogy.cql +5 -5
  8. data/examples/CQL/Metamodel.cql +121 -91
  9. data/examples/CQL/MonthInSeason.cql +2 -6
  10. data/examples/CQL/SeparateSubtype.cql +11 -9
  11. data/examples/CQL/ServiceDirector.cql +21 -33
  12. data/examples/CQL/Supervision.cql +0 -3
  13. data/examples/CQL/WindowInRoomInBldg.cql +10 -4
  14. data/examples/CQL/unit.cql +1 -1
  15. data/lib/activefacts.rb +1 -0
  16. data/lib/activefacts/cql/CQLParser.treetop +5 -1
  17. data/lib/activefacts/cql/Context.treetop +2 -7
  18. data/lib/activefacts/cql/Expressions.treetop +2 -2
  19. data/lib/activefacts/cql/FactTypes.treetop +37 -31
  20. data/lib/activefacts/cql/Language/English.treetop +21 -4
  21. data/lib/activefacts/cql/LexicalRules.treetop +59 -1
  22. data/lib/activefacts/cql/ObjectTypes.treetop +22 -12
  23. data/lib/activefacts/cql/Terms.treetop +13 -9
  24. data/lib/activefacts/cql/ValueTypes.treetop +30 -11
  25. data/lib/activefacts/cql/compiler.rb +34 -5
  26. data/lib/activefacts/cql/compiler/clause.rb +207 -116
  27. data/lib/activefacts/cql/compiler/constraint.rb +129 -105
  28. data/lib/activefacts/cql/compiler/entity_type.rb +49 -27
  29. data/lib/activefacts/cql/compiler/expression.rb +71 -42
  30. data/lib/activefacts/cql/compiler/fact.rb +70 -64
  31. data/lib/activefacts/cql/compiler/fact_type.rb +108 -57
  32. data/lib/activefacts/cql/compiler/query.rb +178 -0
  33. data/lib/activefacts/cql/compiler/shared.rb +13 -12
  34. data/lib/activefacts/cql/compiler/value_type.rb +10 -4
  35. data/lib/activefacts/cql/nodes.rb +1 -1
  36. data/lib/activefacts/cql/parser.rb +6 -2
  37. data/lib/activefacts/generate/absorption.rb +6 -3
  38. data/lib/activefacts/generate/cql.rb +140 -84
  39. data/lib/activefacts/generate/dm.rb +12 -6
  40. data/lib/activefacts/generate/help.rb +25 -6
  41. data/lib/activefacts/generate/helpers/oo.rb +195 -0
  42. data/lib/activefacts/generate/helpers/ordered.rb +589 -0
  43. data/lib/activefacts/generate/helpers/rails.rb +57 -0
  44. data/lib/activefacts/generate/html/glossary.rb +274 -54
  45. data/lib/activefacts/generate/json.rb +25 -22
  46. data/lib/activefacts/generate/null.rb +1 -0
  47. data/lib/activefacts/generate/rails/models.rb +244 -0
  48. data/lib/activefacts/generate/rails/schema.rb +185 -0
  49. data/lib/activefacts/generate/records.rb +1 -0
  50. data/lib/activefacts/generate/ruby.rb +51 -30
  51. data/lib/activefacts/generate/sql/mysql.rb +5 -3
  52. data/lib/activefacts/generate/sql/server.rb +8 -4
  53. data/lib/activefacts/generate/text.rb +1 -0
  54. data/lib/activefacts/generate/transform/surrogate.rb +209 -0
  55. data/lib/activefacts/generate/version.rb +1 -0
  56. data/lib/activefacts/input/orm.rb +234 -181
  57. data/lib/activefacts/mapping/rails.rb +122 -0
  58. data/lib/activefacts/persistence/columns.rb +34 -18
  59. data/lib/activefacts/persistence/foreignkey.rb +129 -71
  60. data/lib/activefacts/persistence/index.rb +42 -12
  61. data/lib/activefacts/persistence/reference.rb +37 -23
  62. data/lib/activefacts/persistence/tables.rb +53 -19
  63. data/lib/activefacts/registry.rb +11 -0
  64. data/lib/activefacts/support.rb +28 -10
  65. data/lib/activefacts/version.rb +1 -1
  66. data/lib/activefacts/vocabulary/extensions.rb +246 -117
  67. data/lib/activefacts/vocabulary/metamodel.rb +105 -65
  68. data/lib/activefacts/vocabulary/verbaliser.rb +226 -194
  69. data/spec/absorption_spec.rb +1 -0
  70. data/spec/cql/comparison_spec.rb +8 -8
  71. data/spec/cql/contractions_spec.rb +16 -43
  72. data/spec/cql/entity_type_spec.rb +2 -1
  73. data/spec/cql/expressions_spec.rb +2 -2
  74. data/spec/cql/fact_type_matching_spec.rb +4 -1
  75. data/spec/cql/parser/bad_literals_spec.rb +30 -30
  76. data/spec/cql/parser/entity_types_spec.rb +6 -6
  77. data/spec/cql/parser/expressions_spec.rb +25 -19
  78. data/spec/cql/samples_spec.rb +5 -4
  79. data/spec/cql_cql_spec.rb +2 -1
  80. data/spec/cql_dm_spec.rb +4 -0
  81. data/spec/cql_mysql_spec.rb +4 -0
  82. data/spec/cql_parse_spec.rb +2 -0
  83. data/spec/cql_ruby_spec.rb +4 -0
  84. data/spec/cql_sql_spec.rb +4 -0
  85. data/spec/cqldump_spec.rb +7 -4
  86. data/spec/helpers/parse_to_ast_matcher.rb +7 -3
  87. data/spec/helpers/test_parser.rb +2 -0
  88. data/spec/norma_cql_spec.rb +5 -2
  89. data/spec/norma_ruby_spec.rb +4 -1
  90. data/spec/norma_ruby_sql_spec.rb +4 -1
  91. data/spec/norma_sql_spec.rb +4 -1
  92. data/spec/norma_tables_spec.rb +2 -2
  93. data/spec/ruby_api_spec.rb +1 -1
  94. data/spec/spec_helper.rb +2 -0
  95. data/spec/transform_surrogate_spec.rb +59 -0
  96. metadata +70 -60
  97. data/TODO +0 -308
  98. data/lib/activefacts/cql/compiler/join.rb +0 -162
  99. data/lib/activefacts/generate/oo.rb +0 -176
  100. data/lib/activefacts/generate/ordered.rb +0 -602
@@ -0,0 +1,122 @@
1
+ require 'activefacts/vocabulary'
2
+ require 'activefacts/persistence'
3
+ require 'active_support'
4
+ require 'digest/sha1'
5
+
6
+ module ActiveFacts
7
+
8
+ module Persistence
9
+ # Return ActiveRecord type and (modified?) length for the passed base type
10
+ def self.rails_type(type, length)
11
+ rails_type = case type
12
+ when /^Auto ?Counter$/
13
+ 'integer' # REVISIT: Need to detect surrogate ID fields and handle them correctly
14
+
15
+ when /^Unsigned ?Integer$/,
16
+ /^Integer$/,
17
+ /^Signed ?Integer$/,
18
+ /^Unsigned ?Small ?Integer$/,
19
+ /^Signed ?Small ?Integer$/,
20
+ /^Unsigned ?Tiny ?Integer$/
21
+ length = nil
22
+ 'integer'
23
+
24
+ when /^Decimal$/
25
+ 'decimal'
26
+
27
+ when /^Fixed ?Length ?Text$/, /^Char$/
28
+ 'string'
29
+ when /^Variable ?Length ?Text$/, /^String$/
30
+ 'string'
31
+ when /^Large ?Length ?Text$/, /^Text$/
32
+ 'text'
33
+
34
+ when /^Date ?And ?Time$/, /^Date ?Time$/
35
+ 'datetime'
36
+ when /^Date$/
37
+ 'datetime'
38
+ when /^Time$/
39
+ 'time'
40
+ when /^Auto ?Time ?Stamp$/
41
+ 'timestamp'
42
+
43
+ when /^Money$/
44
+ 'decimal'
45
+ when /^Picture ?Raw ?Data$/, /^Image$/, /^Variable ?Length ?Raw ?Data$/, /^Blob$/
46
+ 'binary'
47
+ when /^BIT$/
48
+ 'boolean'
49
+ else type # raise "ActiveRecord type unknown for standard type #{type}"
50
+ end
51
+ [rails_type, length]
52
+ end
53
+
54
+ def self.rails_plural_name name
55
+ # Crunch spaces and pluralise the first part, all in snake_case
56
+ name.pop if name.is_a?(Array) and name.last == []
57
+ name = name[0]*'_' if name.is_a?(Array) and name.size == 1
58
+ if name.is_a?(Array)
59
+ name = ActiveSupport::Inflector.tableize((name[0]*'_').gsub(/\s+/, '_')) +
60
+ '_' +
61
+ ActiveSupport::Inflector.underscore((name[1..-1].flatten*'_').gsub(/\s+/, '_'))
62
+ else
63
+ ActiveSupport::Inflector.tableize(name.gsub(/\s+/, '_'))
64
+ end
65
+ end
66
+
67
+ def self.rails_singular_name name
68
+ # Crunch spaces and convert to snake_case
69
+ name = name.flatten*'_' if name.is_a?(Array)
70
+ ActiveSupport::Inflector.underscore(name.gsub(/\s+/, '_'))
71
+ end
72
+
73
+ class Column
74
+ def rails_name
75
+ Persistence::rails_singular_name(name('_'))
76
+ end
77
+ end
78
+
79
+ class Index
80
+ def rails_name
81
+ column_names = columns.map{|c| c.rails_name }
82
+ index_name = "index_#{on.rails_name+'_on_'+column_names*'_'}"
83
+ if index_name.length > 63
84
+ hash = Digest::SHA1.hexdigest index_name
85
+ index_name = index_name[0, 53] + '__' + hash[0, 8]
86
+ end
87
+ index_name
88
+ end
89
+ end
90
+
91
+ class ForeignKey
92
+ def rails_from_association_name
93
+ Persistence::rails_singular_name(to_name.join('_'))
94
+ end
95
+
96
+ def rails_to_association
97
+ jump = jump_reference
98
+ if jump.is_one_to_one
99
+ [ "has_one", Persistence::rails_singular_name(from_name)]
100
+ else
101
+ [ "has_many", Persistence::rails_plural_name(from_name)]
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ module Metamodel
108
+ class ObjectType
109
+ def rails_name
110
+ Persistence::rails_plural_name(name)
111
+ end
112
+
113
+ def rails_singular_name
114
+ Persistence::rails_singular_name(name)
115
+ end
116
+
117
+ def rails_class_name
118
+ ActiveSupport::Inflector.camelize(name.gsub(/\s+/, '_'))
119
+ end
120
+ end
121
+ end
122
+ end
@@ -55,18 +55,18 @@ module ActiveFacts
55
55
  end
56
56
 
57
57
  # A Column name is a sequence of names (derived from the to_roles of the References)
58
- # joined by a joiner string (pass nil to get the original array of names)
58
+ # appended by a separator string (pass nil to get the original array of names)
59
59
  # The names to use is derived from the to_names of each Reference,
60
60
  # modified by these rules:
61
61
  # * A reference after the first one which is not a TypeInheritance but where the _from_ object plays the sole role in the preferred identifier of the _to_ entity is ignored,
62
62
  # * A reference (after a name has been retained) which is a TypeInheritance retains the names of the subtype,
63
63
  # * If the names retained so far end in XYZ and the to_names start with XYZ, remove the duplication
64
64
  # * If we have retained the name of an entity, and this reference is the sole identifying role of an entity, and the identifying object has a name that is prefixed by the name of the object it identifies, remove the prefix and use just the suffix.
65
- def name(joiner = "")
66
- self.class.name(@references, joiner)
65
+ def name(separator = "")
66
+ self.class.name(@references, separator)
67
67
  end
68
68
 
69
- def self.name(refs, joiner = "")
69
+ def self.name(refs, separator = "")
70
70
  last_names = []
71
71
  names = refs.
72
72
  inject([]) do |a, ref|
@@ -82,7 +82,7 @@ module ActiveFacts
82
82
  next a
83
83
  end
84
84
 
85
- names = ref.to_names
85
+ names = ref.to_names(ref != refs.last)
86
86
 
87
87
  # When traversing type inheritances, keep the subtype name, not the supertype names as well:
88
88
  if a.size > 0 && ref.fact_type.is_a?(ActiveFacts::Metamodel::TypeInheritance)
@@ -131,17 +131,23 @@ module ActiveFacts
131
131
  }
132
132
 
133
133
  name_array = names.map{|n| n.sub(/^[a-z]/){|s| s.upcase}}
134
- joiner ? name_array * joiner : name_array
134
+ separator ? name_array * separator : name_array
135
135
  end
136
136
 
137
137
  # Is this column mandatory or nullable?
138
138
  def is_mandatory
139
- !@references.detect{|ref| !ref.is_mandatory}
139
+ # Uncomment the following line for CWA unaries (not nullable, just T/F)
140
+ # @references[-1].is_unary ||
141
+ !@references.detect{|ref| !ref.is_mandatory || ref.is_unary }
140
142
  end
141
143
 
142
- # Is this column an auto-assigned value type?
144
+ # This column is auto-assigned if it's an auto-assigned value type and is not a foreign key
143
145
  def is_auto_assigned
144
- (to = references[-1].to) && to.is_auto_assigned
146
+ last_table_ref = references.reverse.detect{|r| r.from && r.from.is_table}
147
+ (to = references[-1].to) &&
148
+ to.is_auto_assigned &&
149
+ references[0].from.identifier_columns.size == 1 &&
150
+ references[0].from == last_table_ref.from
145
151
  end
146
152
 
147
153
  # What's the underlying SQL data type of this column?
@@ -168,7 +174,7 @@ module ActiveFacts
168
174
  return [vt.name, params, constraints]
169
175
  end
170
176
 
171
- # The comment is the readings from the References expressed as a join
177
+ # The comment is the readings from the References expressed as a series of steps (not a full verbalisation)
172
178
  def comment
173
179
  @references.map do |ref|
174
180
  (ref.is_mandatory ? "" : "maybe ") +
@@ -186,9 +192,10 @@ module ActiveFacts
186
192
  def columns(excluded_supertypes) #:nodoc:
187
193
  kind = ""
188
194
  cols =
189
- if is_unary && !(@to && @to.fact_type)
195
+ if is_unary
190
196
  kind = "unary "
191
- [Column.new()]
197
+ [Column.new()] +
198
+ ((@to && @to.fact_type) ? @to.all_columns(excluded_supertypes) : [])
192
199
  elsif is_self_value
193
200
  kind = "self-role "
194
201
  [Column.new()]
@@ -218,7 +225,7 @@ module ActiveFacts
218
225
  class ObjectType
219
226
  # The array of columns for this ObjectType's table
220
227
  def columns
221
- @columns
228
+ @columns || populate_columns
222
229
  end
223
230
 
224
231
  def populate_columns #:nodoc:
@@ -229,12 +236,16 @@ module ActiveFacts
229
236
 
230
237
  # The ValueType class is defined in the metamodel; full documentation is not generated.
231
238
  # This section shows the features relevant to relational Persistence.
232
- class ValueType < ObjectType
239
+ class ValueType < DomainObjectType
233
240
  # The identifier_columns for a ValueType can only ever be the self-value role that was injected
234
241
  def identifier_columns
235
242
  debug :columns, "Identifier Columns for #{name}" do
236
243
  raise "Illegal call to identifier_columns for absorbed ValueType #{name}" unless is_table
237
- columns.select{|column| column.references[0] == self_value_reference}
244
+ if isr = injected_surrogate_role
245
+ columns.select{|column| column.references[0].from_role == isr }
246
+ else
247
+ columns.select{|column| column.references[0] == self_value_reference}
248
+ end
238
249
  end
239
250
  end
240
251
 
@@ -243,7 +254,12 @@ module ActiveFacts
243
254
  def reference_columns(excluded_supertypes) #:nodoc:
244
255
  debug :columns, "Reference Columns for #{name}" do
245
256
  if is_table
246
- [ActiveFacts::Persistence::Column.new(self_value_reference)]
257
+ if isr = injected_surrogate_role
258
+ ref_from = references_from.detect{|ref| ref.from_role == isr}
259
+ [ActiveFacts::Persistence::Column.new(ref_from)]
260
+ else
261
+ [ActiveFacts::Persistence::Column.new(self_value_reference)]
262
+ end
247
263
  else
248
264
  [ActiveFacts::Persistence::Column.new]
249
265
  end
@@ -278,7 +294,7 @@ module ActiveFacts
278
294
 
279
295
  # The EntityType class is defined in the metamodel; full documentation is not generated.
280
296
  # This section shows the features relevant to relational Persistence.
281
- class EntityType < ObjectType
297
+ class EntityType < DomainObjectType
282
298
  # The identifier_columns for an EntityType are the columns that result from the identifying roles
283
299
  def identifier_columns
284
300
  debug :columns, "Identifier Columns for #{name}" do
@@ -318,7 +334,7 @@ module ActiveFacts
318
334
  # REVISIT: Should index references by to_role:
319
335
  ref = references_from.detect {|ref| ref.to_role == role_ref.role}
320
336
 
321
- raise "reference for role #{role.describe} not found on #{name} in #{references_from.size} references:\n\t#{references_from.map(&:to_s)*"\n\t"}" unless ref
337
+ raise "reference for role #{role_ref.describe} not found on #{name} in #{references_from.size} references:\n\t#{references_from.map(&:to_s)*"\n\t"}" unless ref
322
338
 
323
339
  ref.columns({})
324
340
  end.flatten
@@ -14,7 +14,7 @@ module ActiveFacts
14
14
  def to; @to; end
15
15
 
16
16
  # What reference created the FK?
17
- def reference; @fk_ref; end
17
+ def references; @references; end
18
18
 
19
19
  # What columns in the *from* table form the FK
20
20
  def from_columns; @from_columns; end
@@ -22,10 +22,71 @@ module ActiveFacts
22
22
  # What columns in the *to* table form the identifier
23
23
  def to_columns; @to_columns; end
24
24
 
25
- def initialize(from, to, fk_ref, from_columns, to_columns) #:nodoc:
26
- @from, @to, @fk_ref, @from_columns, @to_columns =
27
- from, to, fk_ref, from_columns, to_columns
25
+ def initialize(from, to, references, from_columns, to_columns) #:nodoc:
26
+ @from, @to, @references, @from_columns, @to_columns =
27
+ from, to, references, from_columns, to_columns
28
28
  end
29
+
30
+ def describe
31
+ "foreign key from #{from.name}(#{from_columns.map{|c| c.name}*', '}) to #{to.name}(#{to_columns.map{|c| c.name}*', '})"
32
+ end
33
+
34
+ def verbalised_path
35
+ # REVISIT: This should be a proper join path verbalisation:
36
+ references.map do |r|
37
+ (r.fact_type.entity_type ? r.fact_type.entity_type.name + ' (in which ' : '') +
38
+ r.fact_type.default_reading +
39
+ (r.fact_type.entity_type ? ')' : '')
40
+ end * ' and '
41
+ end
42
+
43
+ # Which references are absorbed into the "from" table?
44
+ def precursor_references
45
+ fk_jump = @references.detect(&:fk_jump)
46
+ jump_index = @references.index(fk_jump)
47
+ @references[0, jump_index]
48
+ end
49
+
50
+ # Which references are absorbed into the "to" table?
51
+ def following_references
52
+ fk_jump = @references.detect(&:fk_jump)
53
+ jump_index = @references.index(fk_jump)
54
+ fk_jump != @references.last ? @references[jump_index+1..-1] : []
55
+ end
56
+
57
+ def jump_reference
58
+ @references.detect(&:fk_jump)
59
+ end
60
+
61
+ def to_name
62
+ p = precursor_references
63
+ f = following_references
64
+ j = jump_reference
65
+
66
+ @references.last.to_names +
67
+ (p.empty? && f.empty? ? [] : ['via'] + p.map{|r| r.to_names}.flatten + f.map{|r| r.from_names}.flatten)
68
+ end
69
+
70
+ # The from_name is the role name of the table with the FK, viewed from the other end
71
+ # When there are no precursor_references or following_references, it's the jump_reference.from_names
72
+ # REVISIT: I'm still working out what to do with precursor_references and following_references
73
+ def from_name
74
+ p = precursor_references
75
+ f = following_references
76
+ j = jump_reference
77
+
78
+ # pluralise unless j.is_one_to_one
79
+
80
+ # REVISIT: references[0].from_names is where the FK lives; but the object of interest may be an absorbed subclass which we should use here instead:
81
+ # REVISIT: Should crunch superclasses in subtype traversals
82
+ # REVISIT: Need to add "_as_rolename" where rolename is not to.name
83
+
84
+ [
85
+ @references[0].from_names,
86
+ (p.empty? && f.empty? ? [] : ['via'] + p.map{|r| r.to_names}.flatten + f.map{|r| r.from_names}.flatten)
87
+ ]
88
+ end
89
+
29
90
  end
30
91
  end
31
92
 
@@ -43,87 +104,84 @@ module ActiveFacts
43
104
  # REVISIT: Disabled, as this should never happen.
44
105
  # next array if ref.to.absorbed_via != ref.fact_type
45
106
  end
107
+ ref.fk_jump = true
46
108
  array << [ref]
47
- elsif ref.is_absorbing
48
- ref.to.all_absorbed_foreign_key_reference_path.each{|aref|
49
- array << aref.insert(0, ref)
50
- }
109
+ elsif ref.is_absorbing or (ref.to && !ref.to.is_table)
110
+ debug :fk, "getting fks absorbed into #{name} via #{ref}" do
111
+ ref.to.all_absorbed_foreign_key_reference_path.each do |aref|
112
+ array << aref.insert(0, ref)
113
+ end
114
+ end
51
115
  end
52
116
  array
53
117
  end
54
118
  end
55
119
 
120
+ def foreign_keys_to
121
+ @foreign_keys_to ||= []
122
+ end
123
+
56
124
  # Return an array of all the foreign keys from this table
57
125
  def foreign_keys
58
- fk_ref_paths = all_absorbed_foreign_key_reference_path
59
126
 
60
127
  # Get the ForeignKey object for each absorbed reference path
61
- fk_ref_paths.map do |fk_ref_path|
62
- debug :fk, "\nFK: " + fk_ref_path.map{|fk_ref| fk_ref.reading }*" and " do
63
-
64
- from_columns = (columns||all_columns({})).select{|column|
65
- column.references[0...fk_ref_path.size] == fk_ref_path
66
- }
67
- debug :fk, "from_columns = #{from_columns.map { |column| column.name }*", "}"
68
-
69
- absorption_path = []
70
- to = fk_ref_path.last.to
71
- # REVISIT: There should be a better way to find where it's absorbed (especially since this fails for absorbed subtypes having their own identification!)
72
- while (r = to.absorbed_via)
73
- absorption_path << r
74
- to = r.to == to ? r.from : r.to
75
- end
76
- raise "REVISIT: #{fk_ref_path.inspect} is bad" unless to and to.columns
128
+ @foreign_keys ||=
129
+ begin
130
+ fk_ref_paths = all_absorbed_foreign_key_reference_path
131
+ fk_ref_paths.map do |fk_ref_path|
132
+ debug :fk, "\nFK: " + fk_ref_path.map{|fk_ref| fk_ref.reading }*" and " do
77
133
 
78
- unless absorption_path.empty?
79
- debug :fk, "Reference target #{fk_ref_path.last.to.name} is absorbed into #{to.name} via:" do
80
- debug :fk, "#{absorption_path.map(&:reading)*" and "}"
81
- end
82
- end
134
+ from_columns = (columns||all_columns({})).select{|column|
135
+ column.references[0...fk_ref_path.size] == fk_ref_path
136
+ }
137
+ debug :fk, "from_columns = #{from_columns.map { |column| column.name }*", "}"
83
138
 
84
- debug :fk, "Looking at absorption depth of #{absorption_path.size} in #{to.name} for to_columns for #{from_columns.map(&:name)*", "}:"
85
- to_supertypes = to.supertypes_transitive
86
- to_columns = from_columns.map do |from_column|
87
- debug :fk, "\tLooking for counterpart of #{from_column.name}: #{from_column.comment}" do
88
- target_path = absorption_path + from_column.references[fk_ref_path.size..-1]
89
- debug :fk, "\tcounterpart MUST MATCH #{target_path.map(&:reading)*" and "}"
90
- c = to.columns.detect do |column|
91
- debug :fk, "Considering #{column.references.map(&:reading) * " and "}"
92
- debug :fk, "exact match: #{column.name}: #{column.comment}" if column.references == target_path
93
- # Column may be inherited into "to", in which case target_path is too long.
94
- cr = column.references
95
- allowed_type = fk_ref_path.last.to
96
- #debug :fk, "Check for absorption, need #{allowed_type.name}" if cr != target_path
97
- cr == target_path or
98
- cr == target_path[-cr.size..-1] &&
99
- !target_path[0...-cr.size].detect do |ref|
100
- ft = ref.fact_type
101
- next true if allowed_type.absorbed_via != ref # Problems if it doesn't match
102
- allowed_type = ref.from
103
- false
104
- end
105
- end
106
- raise "REVISIT: Failed to find conterpart column for #{from_column.name}" unless c
107
- c
108
- end
109
- end
110
- debug :fk, "to_columns in #{to.name}: #{to_columns.map { |column| column ? column.name : "OOPS!" }*", "}"
139
+ # Figure out absorption on the target end:
140
+ to = fk_ref_path.last.to
141
+ if to.absorbed_via
142
+ debug :fk, "Reference target #{fk_ref_path.last.to.name} is absorbed via:" do
143
+ while (r = to.absorbed_via)
144
+ m = r.reversed
145
+ debug :fk, "#{m.reading}"
146
+ fk_ref_path << m
147
+ to = m.from == to ? m.to : m.from
148
+ end
149
+ debug :fk, "Absorption ends at #{to.name}"
150
+ end
151
+ end
111
152
 
112
- # Put the column pairs in a defined order, sorting key pairs by to-name:
113
- froms, tos = from_columns.zip(to_columns).sort_by { |pair|
114
- pair[1].name(nil)
115
- }.transpose
153
+ # REVISIT: This test may no longer be necessary
154
+ raise "REVISIT: #{fk_ref_path.inspect} is bad" unless to and to.columns
155
+
156
+ # REVISIT: This fails for absorbed subtypes having their own identification.
157
+ # Check the CompanyDirectorEmployee model for example, EmployeeManagerNr -> Person (should reference EmployeeNr)
158
+ # Need to use the absorbed identifier_columns of the subtype,
159
+ # not the columns of the supertype that absorbs it.
160
+ # But in general, that isn't going to work because in most DBMS
161
+ # there's no suitable uniquen index on the subtype's identifier_columns
162
+
163
+ to_columns = fk_ref_path[-1].to.identifier_columns
164
+
165
+ # Put the column pairs in the correct order. They MUST be in the order they appear in the primary key
166
+ froms, tos = from_columns.zip(to_columns).sort_by { |pair|
167
+ to_columns.index(pair[1])
168
+ }.transpose
169
+
170
+ fk = ActiveFacts::Persistence::ForeignKey.new(self, to, fk_ref_path, froms, tos)
171
+ to.foreign_keys_to << fk
172
+ fk
173
+ end
174
+ end.
175
+ sort_by do |fk|
176
+ # Put the foreign keys in a defined order:
177
+ # debugger if !fk.to_columns || fk.to_columns.include?(nil) || !fk.from_columns || fk.from_columns.include?(nil)
178
+ [ fk.to.name,
179
+ fk.to_columns.map{|col| col.name(nil).sort},
180
+ fk.from_columns.map{|col| col.name(nil).sort}
181
+ ]
182
+ end
183
+ end
116
184
 
117
- ActiveFacts::Persistence::ForeignKey.new(self, to, fk_ref_path[-1], froms, tos)
118
- end
119
- end.
120
- sort_by do |fk|
121
- # Put the foreign keys in a defined order:
122
- [ fk.to.name,
123
- fk.to_columns.map{|col| col.name(nil).sort},
124
- fk.from_columns.map{|col| col.name(nil).sort}
125
- ]
126
- end
127
185
  end
128
186
  end
129
187
  end