activefacts-generators 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +4 -0
  5. data/Gemfile +10 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +30 -0
  8. data/Rakefile +6 -0
  9. data/activefacts-generators.gemspec +26 -0
  10. data/lib/activefacts/dependency_analyser.rb +182 -0
  11. data/lib/activefacts/generators/absorption.rb +71 -0
  12. data/lib/activefacts/generators/composition.rb +119 -0
  13. data/lib/activefacts/generators/cql.rb +715 -0
  14. data/lib/activefacts/generators/diagrams/json.rb +340 -0
  15. data/lib/activefacts/generators/help.rb +64 -0
  16. data/lib/activefacts/generators/helpers/inject.rb +16 -0
  17. data/lib/activefacts/generators/helpers/oo.rb +162 -0
  18. data/lib/activefacts/generators/helpers/ordered.rb +605 -0
  19. data/lib/activefacts/generators/helpers/rails.rb +57 -0
  20. data/lib/activefacts/generators/html/glossary.rb +462 -0
  21. data/lib/activefacts/generators/metadata/json.rb +204 -0
  22. data/lib/activefacts/generators/null.rb +32 -0
  23. data/lib/activefacts/generators/rails/models.rb +247 -0
  24. data/lib/activefacts/generators/rails/schema.rb +217 -0
  25. data/lib/activefacts/generators/ruby.rb +134 -0
  26. data/lib/activefacts/generators/sql/mysql.rb +281 -0
  27. data/lib/activefacts/generators/sql/server.rb +274 -0
  28. data/lib/activefacts/generators/stats.rb +70 -0
  29. data/lib/activefacts/generators/text.rb +29 -0
  30. data/lib/activefacts/generators/traits/datavault.rb +241 -0
  31. data/lib/activefacts/generators/traits/oo.rb +73 -0
  32. data/lib/activefacts/generators/traits/ordered.rb +33 -0
  33. data/lib/activefacts/generators/traits/ruby.rb +210 -0
  34. data/lib/activefacts/generators/transform/datavault.rb +303 -0
  35. data/lib/activefacts/generators/transform/surrogate.rb +215 -0
  36. data/lib/activefacts/registry.rb +11 -0
  37. metadata +176 -0
@@ -0,0 +1,73 @@
1
+ #
2
+ # ActiveFacts Generators.
3
+ # Base class for generators of class libraries in any object-oriented language that supports the ActiveFacts API.
4
+ #
5
+ # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
6
+ #
7
+ module ActiveFacts
8
+ module Generators
9
+ module OOTraits
10
+ module ObjectType
11
+ # Map the ObjectType name to an OO class name
12
+ def oo_type_name
13
+ name.words.capcase
14
+ end
15
+
16
+ # Map the OO class name to a default role name
17
+ def oo_default_role_name
18
+ name.words.snakecase
19
+ end
20
+ end
21
+
22
+ module Role
23
+ def oo_role_definition
24
+ return if fact_type.entity_type
25
+
26
+ if fact_type.all_role.size == 1
27
+ return " maybe :#{preferred_role_name}\n"
28
+ elsif fact_type.all_role.size != 2
29
+ # Shouldn't come here, except perhaps for an invalid model
30
+ return # ternaries and higher are always objectified
31
+ end
32
+
33
+ # REVISIT: TypeInheritance
34
+ if fact_type.is_a?(ActiveFacts::Metamodel::TypeInheritance)
35
+ # trace "Ignoring role #{self} in #{fact_type}, subtype fact type"
36
+ # REVISIT: What about secondary subtypes?
37
+ # REVISIT: What about dumping the relational mapping when using separate tables?
38
+ return
39
+ end
40
+
41
+ return unless is_functional
42
+
43
+ counterpart_role = fact_type.all_role.select{|r| r != self}[0]
44
+ counterpart_type = counterpart_role.object_type
45
+ counterpart_role_name = counterpart_role.preferred_role_name
46
+ counterpart_type_default_role_name = counterpart_type.oo_default_role_name
47
+
48
+ # It's a one_to_one if there's a uniqueness constraint on the other role:
49
+ one_to_one = counterpart_role.is_functional
50
+ return if one_to_one &&
51
+ false # REVISIT: !@object_types_dumped[counterpart_role.object_type]
52
+
53
+ # Find role name:
54
+ role_method = preferred_role_name
55
+ counterpart_role_method = one_to_one ? role_method : "all_"+role_method
56
+ # puts "---"+role.role_name if role.role_name
57
+ if counterpart_role_name != counterpart_type.oo_default_role_name and
58
+ role_method == self.object_type.oo_default_role_name
59
+ # debugger
60
+ counterpart_role_method += "_as_#{counterpart_role_name}"
61
+ end
62
+
63
+ role_name = role_method
64
+ role_name = nil if role_name == object_type.oo_default_role_name
65
+
66
+ as_binary(counterpart_role_name, counterpart_type, is_mandatory, one_to_one, nil, role_name, counterpart_role_method)
67
+ end
68
+ end
69
+
70
+ include ActiveFacts::TraitInjector # Must be last in this module, after all submodules have been defined
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,33 @@
1
+ #
2
+ # ActiveFacts Generators.
3
+ # Generation support superclass that sequences entity types to avoid forward references.
4
+ #
5
+ # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
6
+ #
7
+ module ActiveFacts
8
+ module Generators #:nodoc:
9
+ module OrderedTraits
10
+ module DumpedFlag
11
+ attr_reader :ordered_dumped
12
+
13
+ def ordered_dumped!
14
+ @ordered_dumped = true
15
+ end
16
+ end
17
+
18
+ module ObjectType
19
+ include DumpedFlag
20
+ end
21
+
22
+ module FactType
23
+ include DumpedFlag
24
+ end
25
+
26
+ module Constraint
27
+ include DumpedFlag
28
+ end
29
+
30
+ include ActiveFacts::TraitInjector # Must be last in this module, after all submodules have been defined
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,210 @@
1
+ #
2
+ # ActiveFacts Generators.
3
+ # Generate Ruby classes for the ActiveFacts API from an ActiveFacts vocabulary.
4
+ #
5
+ # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
6
+ #
7
+ module ActiveFacts
8
+ module Generators
9
+ module RubyTraits
10
+ module Vocabulary
11
+ def prelude
12
+ if @mapping == 'sql'
13
+ require 'activefacts/rmap'
14
+ @tables = self.tables
15
+ end
16
+
17
+ "require 'activefacts/api'\n" +
18
+ (@mapping == 'sql' ? "require 'activefacts/rmap'\n" : '') +
19
+ "\nmodule ::#{self.name}\n\n"
20
+ end
21
+
22
+ def finale
23
+ "end"
24
+ end
25
+ end
26
+
27
+ module ObjectType
28
+ def absorbed_roles
29
+ all_role.
30
+ select do |role|
31
+ role.fact_type.all_role.size <= 2 &&
32
+ !role.fact_type.is_a?(ActiveFacts::Metamodel::LinkFactType)
33
+ end.
34
+ sort_by do |role|
35
+ r = role.fact_type.all_role.select{|r2| r2 != role}[0] || role
36
+ r.preferred_role_name(self) + ':' + role.preferred_role_name(r.object_type)
37
+ end
38
+ end
39
+
40
+ # Map the ObjectType name to a Ruby class name
41
+ def ruby_type_name
42
+ oo_type_name
43
+ end
44
+
45
+ # Map the Ruby class name to a default role name
46
+ def ruby_default_role_name
47
+ oo_default_role_name
48
+ end
49
+
50
+
51
+ def ruby_type_reference
52
+ if !ordered_dumped
53
+ '"'+name.gsub(/ /,'')+'"'
54
+ else
55
+ role_reference = name.gsub(/ /,'')
56
+ end
57
+ end
58
+ end
59
+
60
+ module Role
61
+ def preferred_role_name(is_for = nil, &name_builder)
62
+
63
+ if fact_type.is_a?(ActiveFacts::Metamodel::TypeInheritance)
64
+ # Subtype and Supertype roles default to TitleCase names, and have no role_name to worry about:
65
+ return (name_builder || proc {|names| names.titlecase}).call(object_type.name.words)
66
+ end
67
+
68
+ name_builder ||= proc {|names| names.map(&:downcase)*'_' } # Make snake_case by default
69
+
70
+ # Handle an objectified unary role:
71
+ if is_for && fact_type.entity_type == is_for && fact_type.all_role.size == 1
72
+ return name_builder.call(object_type.name.words)
73
+ end
74
+
75
+ # trace "Looking for preferred_role_name of #{describe_fact_type(fact_type, self)}"
76
+ reading = fact_type.preferred_reading
77
+ preferred_role_ref = reading.role_sequence.all_role_ref.detect{|reading_rr|
78
+ reading_rr.role == self
79
+ }
80
+
81
+ if fact_type.all_role.size == 1
82
+ return name_builder.call(
83
+ role_name ?
84
+ role_name.snakewords :
85
+ reading.text.gsub(/ *\{0\} */,' ').gsub(/[- ]+/,'_').words
86
+ )
87
+ end
88
+
89
+ if role_name && role_name != ""
90
+ role_words = [role_name]
91
+ else
92
+ role_words = []
93
+
94
+ la = preferred_role_ref.leading_adjective
95
+ role_words += la.words.snakewords if la && la != ""
96
+
97
+ role_words += object_type.name.words.snakewords
98
+
99
+ ta = preferred_role_ref.trailing_adjective
100
+ role_words += ta.words.snakewords if ta && ta != ""
101
+ end
102
+
103
+ # n = role_words.map{|w| w.gsub(/([a-z])([A-Z]+)/,'\1_\2').downcase}*"_"
104
+ n = role_words*'_'
105
+ # trace "\tresult=#{n}"
106
+ return name_builder.call(n.gsub(' ','_').split(/_/))
107
+ end
108
+
109
+ def as_binary(role_name, role_player, mandatory = nil, one_to_one = nil, readings = nil, other_role_name = nil, other_method_name = nil)
110
+ ruby_role_name = ":"+role_name.words.snakecase
111
+
112
+ # Find whether we need the name of the other role player, and whether it's defined yet:
113
+ implied_role_name = role_player.name.gsub(/ /,'').sub(/^[a-z]/) {|i| i.upcase}
114
+ if role_name.camelcase != implied_role_name
115
+ # Only use Class name if it's not implied by the rolename
116
+ role_reference = ":class => "+role_player.ruby_type_reference
117
+ end
118
+
119
+ other_role_name = ":counterpart => :"+other_role_name.gsub(/ /,'_') if other_role_name
120
+
121
+ if vr = role_value_constraint
122
+ value_restriction = ":restrict => #{vr}"
123
+ end
124
+
125
+ options = [
126
+ ruby_role_name,
127
+ role_reference,
128
+ mandatory ? ":mandatory => true" : nil,
129
+ readings,
130
+ other_role_name,
131
+ value_restriction
132
+ ].compact
133
+
134
+ debugger if ruby_role_name == 'astronomicalobject'
135
+
136
+ line = " #{one_to_one ? "one_to_one" : "has_one" } #{options*', '} "
137
+ if other_method_name
138
+ line += " "*(48-line.length) if line.length < 48
139
+ line += "\# See #{role_player.name.gsub(/ /,'')}.#{other_method_name}"
140
+ end
141
+ line+"\n"
142
+ end
143
+
144
+ def ruby_role_definition
145
+ oo_role_definition
146
+ end
147
+ end
148
+
149
+ module ValueType
150
+ def ruby_definition
151
+ return if name == "_ImplicitBooleanValueType"
152
+
153
+ ruby_length = length && length > 0 ? ":length => #{length}" : nil
154
+ ruby_scale = scale && scale > 0 ? ":scale => #{scale}" : nil
155
+ params = [ruby_length,ruby_scale].compact * ", "
156
+
157
+ base_type = supertype || self
158
+ base_type_name = base_type.ruby_type_name
159
+ ruby_name = ruby_type_name
160
+ if base_type_name == ruby_name
161
+ base_type_name = '::'+base_type_name
162
+ end
163
+
164
+ " class #{ruby_name} < #{base_type_name}\n" +
165
+ " value_type #{params}\n" +
166
+ #emit_mapping self if is_table
167
+ (value_constraint ?
168
+ " restrict #{value_constraint.all_allowed_range_sorted.map{|ar| ar.to_s}*", "}\n" :
169
+ ""
170
+ ) +
171
+ (unit ?
172
+ " \# REVISIT: #{ruby_name} is in units of #{unit.name}\n" :
173
+ ""
174
+ ) +
175
+ absorbed_roles.map do |role|
176
+ role.ruby_role_definition
177
+ end.
178
+ compact*"" +
179
+ " end\n\n"
180
+ end
181
+ end
182
+
183
+ module FactType
184
+ # An objectified fact type has internal roles that are always "has_one":
185
+ def fact_roles
186
+ raise "Fact #{describe} type is not objectified" unless entity_type
187
+ all_role.sort_by do |role|
188
+ role.preferred_role_name(entity_type)
189
+ end.
190
+ map do |role|
191
+ role_name = role.preferred_role_name(entity_type)
192
+ one_to_one = role.all_role_ref.detect{|rr|
193
+ rr.role_sequence.all_role_ref.size == 1 &&
194
+ rr.role_sequence.all_presence_constraint.detect{|pc|
195
+ pc.max_frequency == 1
196
+ }
197
+ }
198
+ counterpart_role_method = (one_to_one ? "" : "all_") +
199
+ entity_type.oo_default_role_name +
200
+ (role_name != role.object_type.oo_default_role_name ? "_as_#{role_name}" : '')
201
+ role.as_binary(role_name, role.object_type, true, one_to_one, nil, nil, counterpart_role_method)
202
+ end.
203
+ join('')
204
+ end
205
+ end
206
+
207
+ include ActiveFacts::TraitInjector # Must be last in this module, after all submodules have been defined
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,303 @@
1
+ #
2
+ # Data Vault Transform
3
+ # Transform a loaded ActiveFacts vocabulary to suit Data Vault
4
+ #
5
+ # Copyright (c) 2015 Infinuendo. Read the LICENSE file.
6
+ #
7
+ require 'activefacts/metamodel'
8
+ require 'activefacts/rmap'
9
+ require 'activefacts/registry'
10
+
11
+ require 'activefacts/generators/traits/datavault'
12
+
13
+ module ActiveFacts
14
+
15
+ module Generators #:nodoc:
16
+ module Transform #:nodoc:
17
+ class DataVault
18
+ def initialize(vocabulary, *options)
19
+ @vocabulary = vocabulary
20
+ @constellation = vocabulary.constellation
21
+ end
22
+
23
+ def classify_tables
24
+ initial_tables = @vocabulary.tables
25
+ non_reference_tables = initial_tables.reject do |table|
26
+ table.concept.all_concept_annotation.detect{|ca| ca.mapping_annotation == 'static'} or
27
+ !table.is_a?(ActiveFacts::Metamodel::EntityType)
28
+ end
29
+ @reference_tables = initial_tables-non_reference_tables
30
+
31
+ @link_tables, @hub_tables = non_reference_tables.partition do |table|
32
+ identifying_references = table.identifier_columns.map{|c| c.references.first}.uniq
33
+ # Which identifying_references are played by other tables?
34
+ ir_tables =
35
+ identifying_references.select do |r|
36
+ table_referred_to = r.to
37
+ # I have no examples of multi-level absorption, but it's possible, so loop
38
+ while av = table_referred_to.absorbed_via
39
+ table_referred_to = av.from
40
+ end
41
+ table_referred_to.is_table
42
+ end
43
+ ir_tables.size > 1
44
+ end
45
+ trace_table_classifications
46
+ end
47
+
48
+ def trace_table_classifications
49
+ # Trace the decisions about table types:
50
+ if trace :datavault
51
+ [@reference_tables, @hub_tables, @link_tables].zip(['Reference', 'Hub', 'Link']).each do |tables, kind|
52
+ trace :datavault, kind+' tables:' do
53
+ tables.each do |table|
54
+ identifying_references = table.identifier_columns.map{|c| c.references.first}.uniq
55
+ trace :datavault, "#{table.name}(#{identifying_references.map{|r| (t = r.to) && t.name || 'self'}*', '})"
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ def detect_required_surrogates
63
+ trace :datavault, "Detecting required surrogates" do
64
+ @required_surrogates =
65
+ (@hub_tables+@link_tables).select do |table|
66
+ table.dv_needs_surrogate
67
+ end
68
+ end
69
+ end
70
+
71
+ def inject_required_surrogates
72
+ trace :datavault, "Injecting any required surrogates" do
73
+ trace :datavault, "Need to inject surrogates into #{@required_surrogates.map(&:name)*', '}"
74
+ @required_surrogates.each do |table|
75
+ table.dv_inject_surrogate
76
+ end
77
+ end
78
+ end
79
+
80
+ def classify_satellite_references table
81
+ identifying_references = table.identifier_columns.map{|c| c.references.first}.uniq
82
+ non_identifying_references = table.columns.map{|c| c.references[0]}.uniq - identifying_references
83
+
84
+ # Skip this table if no satellite data is needed
85
+ # REVISIT: Needed anyway for a link?
86
+ if non_identifying_references.size == 0
87
+ return nil
88
+ end
89
+
90
+ satellites = non_identifying_references.inject({}) do |hash, ref|
91
+ # Extract the declared satellite name, or use just "satellite"
92
+ satellite_subname =
93
+ ref.fact_type.internal_presence_constraints.map do |pc|
94
+ next if !pc.max_frequency || pc.max_frequency > 1 # Not a Uniqueness Constraint
95
+ next if pc.role_sequence.all_role_ref.size > 1 # Covers more than one role
96
+ next if pc.role_sequence.all_role_ref.single.role.object_type != table # Not a unique attribute
97
+ pc.concept.all_concept_annotation.map do |ca|
98
+ if ca.mapping_annotation =~ /^satellite */
99
+ ca.mapping_annotation.sub(/^satellite +/, '')
100
+ else
101
+ nil
102
+ end
103
+ end
104
+ end.flatten.compact.uniq[0] || table.name
105
+ satellite_name = satellite_subname
106
+ (hash[satellite_name] ||= []) << ref
107
+ hash
108
+ end
109
+ trace :datavault, "#{table.name} satellites are #{satellites.inspect}"
110
+ satellites
111
+ end
112
+
113
+ def create_one_to_many(one, many, predicate_1 = 'has', predicate_2 = 'is of', one_adj = nil)
114
+ # Create a fact type
115
+ fact_type = @constellation.FactType(:concept => :new)
116
+ one_role = @constellation.Role(:concept => :new, :fact_type => fact_type, :ordinal => 0, :object_type => one)
117
+ many_role = @constellation.Role(:concept => :new, :fact_type => fact_type, :ordinal => 1, :object_type => many)
118
+
119
+ # Create two readings
120
+ reading2 = @constellation.Reading(:fact_type => fact_type, :ordinal => 0, :role_sequence => [:new], :text => "{0} #{predicate_2} {1}")
121
+ @constellation.RoleRef(:role_sequence => reading2.role_sequence, :ordinal => 0, :role => many_role)
122
+ @constellation.RoleRef(:role_sequence => reading2.role_sequence, :ordinal => 1, :role => one_role, :leading_adjective => one_adj)
123
+
124
+ reading1 = @constellation.Reading(:fact_type => fact_type, :ordinal => 1, :role_sequence => [:new], :text => "{0} #{predicate_1} {1}")
125
+ @constellation.RoleRef(:role_sequence => reading1.role_sequence, :ordinal => 0, :role => one_role, :leading_adjective => one_adj)
126
+ @constellation.RoleRef(:role_sequence => reading1.role_sequence, :ordinal => 1, :role => many_role)
127
+
128
+ one_id = @constellation.PresenceConstraint(
129
+ :concept => :new,
130
+ :vocabulary => @vocabulary,
131
+ :name => one.name+'HasOne'+many.name,
132
+ :role_sequence => [:new],
133
+ :is_mandatory => true,
134
+ :min_frequency => 1,
135
+ :max_frequency => 1,
136
+ :is_preferred_identifier => false
137
+ )
138
+ @constellation.RoleRef(:role_sequence => one_id.role_sequence, :ordinal => 0, :role => many_role)
139
+ one_role
140
+ end
141
+
142
+ def assert_value_type name, supertype = nil
143
+ @vocabulary.valid_value_type_name(name) ||
144
+ @constellation.ValueType(:vocabulary => @vocabulary, :name => name, :supertype => supertype, :concept => :new)
145
+ end
146
+
147
+ def assert_record_source
148
+ assert_value_type('Record Source', assert_value_type('String'))
149
+ end
150
+
151
+ def assert_date_time
152
+ assert_value_type('Date Time')
153
+ end
154
+
155
+ # Create a PresenceConstraint with two roles, marked as preferred_identifier
156
+ def create_two_role_identifier(r1, r2)
157
+ pc = @constellation.PresenceConstraint(
158
+ :concept => :new,
159
+ :vocabulary => @vocabulary,
160
+ :name => r1.object_type.name+' '+r1.object_type.name+'PK',
161
+ :role_sequence => [:new],
162
+ :is_mandatory => true,
163
+ :min_frequency => 1,
164
+ :max_frequency => 1,
165
+ :is_preferred_identifier => true
166
+ )
167
+ @constellation.RoleRef(:role_sequence => pc.role_sequence, :ordinal => 0, :role => r1)
168
+ @constellation.RoleRef(:role_sequence => pc.role_sequence, :ordinal => 1, :role => r2)
169
+ end
170
+
171
+ def lift_role_to_link(ref, table_role)
172
+ trace :datavault, "Broaden #{ref} into a new link"
173
+ uc = table_role.uniqueness_constraint
174
+ one_to_one_constraint = ref.fact_type.internal_presence_constraints.detect{|pc| pc != uc }
175
+
176
+ # Any query Step or Reading on this fact type should be unaffected
177
+
178
+ # Make a new RoleRef for the uniqueness constraint so it spans
179
+ uc.constellation.RoleRef(uc.role_sequence, 1, :role => ref.to_role)
180
+ one_to_one_constraint.retract if one_to_one_constraint
181
+
182
+ # Add the objectifying entity type:
183
+ et = uc.constellation.EntityType(
184
+ uc.vocabulary,
185
+ "#{ref.from.name} #{ref.to_names*' '}",
186
+ :fact_type => ref.fact_type,
187
+ :concept => :new
188
+ )
189
+ @link_tables << et
190
+ end
191
+
192
+ def create_satellite(table, satellite_name, references)
193
+ satellite_name = satellite_name.words.titlewords*' '+' SAT'
194
+
195
+ # Create a new entity type with record-date fields in its identifier
196
+ trace :datavault, "Creating #{satellite_name} with #{references.size} references"
197
+ satellite = @constellation.EntityType(:vocabulary => @vocabulary, :name => "#{satellite_name}", :concept => [:new, :implication_rule => "datavault"])
198
+ satellite.definitely_table
199
+
200
+ table_role = create_one_to_many(table, satellite)
201
+
202
+ date_time = assert_date_time
203
+ date_time_role = create_one_to_many(date_time, satellite, 'is of', 'was loaded at', 'load')
204
+ create_two_role_identifier(table_role, date_time_role)
205
+
206
+ record_source = assert_record_source
207
+ record_source.length = 64
208
+ record_source_role = create_one_to_many(record_source, satellite, 'is of', 'was loaded from')
209
+
210
+ # Move all roles across to it from the parent table.
211
+ references.each do |ref|
212
+ trace :datavault, "Moving #{ref} across to #{table.name}_#{satellite_name}" do
213
+ table_role = ref.fact_type.all_role.detect{|r| r.object_type == table}
214
+ if table_role
215
+ remote_table = ref.to
216
+ while remote_table.absorbed_via
217
+ absorbed_into = remote_table.absorbed_via.from
218
+ remote_table = absorbed_into
219
+ end
220
+ if @hub_tables.include?(remote_table)
221
+ lift_role_to_link(ref, table_role)
222
+ else
223
+ # Reassign the role player to the satellite:
224
+ table_role.object_type = satellite
225
+ end
226
+ else
227
+ #debugger # Bum, the crappy Reference object bites again.
228
+ $stderr.puts "REVISIT: Can't move the objectified role for #{ref.inspect}. This column will remain in the hub instead of moving to the satellite"
229
+ end
230
+ end
231
+ end
232
+ satellite
233
+ end
234
+
235
+ def generate(out = $stdout)
236
+ @out = out
237
+
238
+ # Strategy:
239
+ # Determine list of ER tables
240
+ # Partition tables into reference tables (annotated), link tables (two+ FKs in PK), and hub tables
241
+ # For each hub and link table
242
+ # Apply a surrogate key if needed (all links, hubs lacking a simple surrogate)
243
+ # Detect references (fact types) leading to all attributes (non-identifying columns)
244
+ # Group attribute facts into satellites (use the satellite annotation if present)
245
+ # For each satellite
246
+ # Create a new entity type with a (hub-key, record-date key)
247
+ # Make new one->many fact type between hub and satellite
248
+ # Modify all attribute facts in this group to attach to the satellite
249
+ # Compute a gresh relational mapping
250
+ # Exclude reference tables and disable enforcement to them
251
+
252
+ classify_tables
253
+
254
+ detect_required_surrogates
255
+
256
+ @sat_tables = []
257
+ trace :datavault, "Creating satellites" do
258
+ (@hub_tables+@link_tables).each do |table|
259
+ satellites = classify_satellite_references table
260
+ next unless satellites
261
+
262
+ trace :datavault, "Creating #{satellites.size} satellites for #{table.name}" do
263
+ satellites.each do |satellite_name, references|
264
+ @sat_tables << create_satellite(table, satellite_name, references)
265
+ end
266
+ end
267
+ end
268
+ end
269
+ trace :datavault, "#{@sat_tables.size} satellite tables created"
270
+
271
+ inject_required_surrogates
272
+
273
+ trace :datavault, "Adding standard fields to hubs and links" do
274
+ (@hub_tables+@link_tables).each do |table|
275
+ date_time = assert_date_time
276
+ date_time_role = create_one_to_many(date_time, table, 'is of', 'was loaded at', 'load')
277
+
278
+ record_source = assert_record_source
279
+ record_source_role = create_one_to_many(record_source, table, 'is of', 'was loaded from')
280
+ end
281
+ end
282
+
283
+ # Now, redo the E-R mapping using the revised schema:
284
+ @vocabulary.decide_tables
285
+
286
+ # Suffix Hub and Link tables with HUB and LINK
287
+ @hub_tables.each { |h| h.name = "#{h.name} HUB"}
288
+ @link_tables.each { |l| l.name = "#{l.name} LINK"}
289
+
290
+ # Before departing, ensure we don't emit the reference tables!
291
+ @reference_tables.each do |table|
292
+ table.definitely_not_table
293
+ @vocabulary.tables.delete(table)
294
+ end
295
+
296
+ end # generate
297
+
298
+ end
299
+ end
300
+ end
301
+ end
302
+
303
+ ActiveFacts::Registry.generator('transform/datavault', ActiveFacts::Generators::Transform::DataVault)