activefacts-generators 1.7.1

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