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