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.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +1 -0
- data/.travis.yml +4 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +30 -0
- data/Rakefile +6 -0
- data/activefacts-generators.gemspec +26 -0
- data/lib/activefacts/dependency_analyser.rb +182 -0
- data/lib/activefacts/generators/absorption.rb +71 -0
- data/lib/activefacts/generators/composition.rb +119 -0
- data/lib/activefacts/generators/cql.rb +715 -0
- data/lib/activefacts/generators/diagrams/json.rb +340 -0
- data/lib/activefacts/generators/help.rb +64 -0
- data/lib/activefacts/generators/helpers/inject.rb +16 -0
- data/lib/activefacts/generators/helpers/oo.rb +162 -0
- data/lib/activefacts/generators/helpers/ordered.rb +605 -0
- data/lib/activefacts/generators/helpers/rails.rb +57 -0
- data/lib/activefacts/generators/html/glossary.rb +462 -0
- data/lib/activefacts/generators/metadata/json.rb +204 -0
- data/lib/activefacts/generators/null.rb +32 -0
- data/lib/activefacts/generators/rails/models.rb +247 -0
- data/lib/activefacts/generators/rails/schema.rb +217 -0
- data/lib/activefacts/generators/ruby.rb +134 -0
- data/lib/activefacts/generators/sql/mysql.rb +281 -0
- data/lib/activefacts/generators/sql/server.rb +274 -0
- data/lib/activefacts/generators/stats.rb +70 -0
- data/lib/activefacts/generators/text.rb +29 -0
- data/lib/activefacts/generators/traits/datavault.rb +241 -0
- data/lib/activefacts/generators/traits/oo.rb +73 -0
- data/lib/activefacts/generators/traits/ordered.rb +33 -0
- data/lib/activefacts/generators/traits/ruby.rb +210 -0
- data/lib/activefacts/generators/transform/datavault.rb +303 -0
- data/lib/activefacts/generators/transform/surrogate.rb +215 -0
- data/lib/activefacts/registry.rb +11 -0
- 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)
|