activefacts 1.5.3 → 1.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Manifest.txt +2 -0
- data/README.rdoc +27 -18
- data/examples/CQL/OilSupply.cql +2 -2
- data/lib/activefacts/cql/FactTypes.treetop +17 -7
- data/lib/activefacts/cql/ObjectTypes.treetop +10 -4
- data/lib/activefacts/cql/compiler/clause.rb +8 -1
- data/lib/activefacts/cql/compiler/constraint.rb +10 -0
- data/lib/activefacts/cql/compiler/fact_type.rb +1 -1
- data/lib/activefacts/generate/traits/datavault.rb +241 -0
- data/lib/activefacts/generate/transform/datavault.rb +266 -0
- data/lib/activefacts/persistence/columns.rb +4 -0
- data/lib/activefacts/persistence/tables.rb +2 -0
- data/lib/activefacts/version.rb +2 -2
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b1e7f3153a8b937d166b295d8b8340f3294888b7
|
4
|
+
data.tar.gz: 74a1f5ddccb7c51b36c6d8e19791ef12841b5c44
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 82841996c8072aa90e40075d787e66fa16726b1048a804301c38cd0e076d02ad963955c6d7bf255bbfbb5b12b83921db132413f6d91ac982db14e28e1528b5c9
|
7
|
+
data.tar.gz: 69fbf20eb22d9ec6bbddb5cc9d9366c0d2e0dced990f140542dfec12409ac550441f15acf48c972c09ece0eb52633eea1cc49cd6b42281ad9f9d5001d17b90d8
|
data/Manifest.txt
CHANGED
@@ -96,6 +96,8 @@ lib/activefacts/generate/topics.rb
|
|
96
96
|
lib/activefacts/generate/traits/oo.rb
|
97
97
|
lib/activefacts/generate/traits/ordered.rb
|
98
98
|
lib/activefacts/generate/traits/ruby.rb
|
99
|
+
lib/activefacts/generate/traits/datavault.rb
|
100
|
+
lib/activefacts/generate/transform/datavault.rb
|
99
101
|
lib/activefacts/generate/transform/surrogate.rb
|
100
102
|
lib/activefacts/generate/version.rb
|
101
103
|
lib/activefacts/input/cql.rb
|
data/README.rdoc
CHANGED
@@ -4,54 +4,63 @@
|
|
4
4
|
|
5
5
|
== DESCRIPTION
|
6
6
|
|
7
|
-
ActiveFacts provides a semantic modeling language, the
|
8
|
-
Query Language (CQL). CQL combines natural language
|
9
|
-
formal logic, producing a formal language that
|
10
|
-
English. ActiveFacts converts semantic models from
|
11
|
-
and object models in SQL, Ruby and other languages.
|
12
|
-
|
13
|
-
The generated models are guaranteed congruent, which
|
14
|
-
object-relational impedance mismatch.
|
7
|
+
ActiveFacts provides a fact-based semantic modeling language, the
|
8
|
+
Constellation Query Language (CQL). CQL combines natural language
|
9
|
+
verbalisation and formal logic, producing a formal language that
|
10
|
+
reads like plain English. ActiveFacts converts semantic models from
|
11
|
+
CQL to relational and object models in SQL, Ruby and other languages.
|
12
|
+
|
13
|
+
The generated models are guaranteed congruent, which can eliminate the
|
14
|
+
object-relational impedance mismatch. Fact based models are much more
|
15
15
|
stable under evolving requirements than either relational or
|
16
16
|
object-oriented models, because they directly express the underlying
|
17
|
-
elementary facts, so are not susceptible to
|
18
|
-
way those attribute-oriented approaches are.
|
17
|
+
conceptual structure as elementary facts, so are not susceptible to
|
18
|
+
ramifying change in the way those attribute-oriented approaches are.
|
19
19
|
|
20
20
|
Semantic modeling is a refinement of fact-based modeling techniques
|
21
21
|
such as ORM2, NIAM and others. ActiveFacts can convert ORM2 files from
|
22
22
|
NORMA to CQL. Fact-based modeling is closely related to relational
|
23
|
-
modeling in the sixth normal form
|
23
|
+
modeling in the sixth normal form (as Codd intended it!), but the
|
24
|
+
generated relation schemas are in 5NF, so they don't suffer from 6NF
|
24
25
|
inefficiency. The relational models it derives are highly efficient.
|
25
26
|
|
26
27
|
== SYNOPSIS:
|
27
28
|
|
29
|
+
afgen --help
|
28
30
|
afgen --sql/server myfile.cql
|
29
31
|
afgen --ruby myfile.cql
|
30
32
|
afgen --cql myfile.orm
|
33
|
+
afgen --transform/surrogate --rails/schema myfile.cql
|
34
|
+
afgen --transform/datavault --sql/server myfile.cql
|
35
|
+
cql (command-line interpreter, including a query evaluator)
|
31
36
|
|
32
37
|
== INSTALL:
|
33
38
|
|
34
39
|
* sudo gem install activefacts
|
35
40
|
|
36
|
-
==
|
41
|
+
== STATUS
|
37
42
|
|
38
|
-
*
|
43
|
+
* The definition language is complete and the main generators are usable.
|
39
44
|
|
40
|
-
*
|
41
|
-
the CQL compiler uses the generated Ruby code extensively)
|
45
|
+
* Arithmetic and aggregate operations in queries are recognised but not compiled.
|
42
46
|
|
43
|
-
*
|
47
|
+
* Queries and derived fact types not yet generated to SQL queries or views..
|
48
|
+
|
49
|
+
* The Constellation API lacks SQL persistence; it has no Object-Relational Mapper
|
50
|
+
(but it's already useful; the CQL compiler uses the generated Ruby code extensively)
|
51
|
+
|
52
|
+
* Advanced constraint types are mostly ignored by the generators.
|
44
53
|
|
45
54
|
== REQUIREMENTS:
|
46
55
|
|
47
56
|
* Treetop parser generator
|
48
57
|
|
49
58
|
* NORMA (see <http://www.ormfoundation.org/files/>), if you want to
|
50
|
-
use ORM (needs Visual Studio Pro edition)
|
59
|
+
use ORM (needs Visual Studio Pro or Community edition)
|
51
60
|
|
52
61
|
== LICENSE:
|
53
62
|
|
54
|
-
Copyright (c) 2008 Clifford Heath.
|
63
|
+
Copyright (c) 2008-2015 Clifford Heath.
|
55
64
|
|
56
65
|
This software is provided 'as-is', without any express or implied warranty.
|
57
66
|
In no event will the authors be held liable for any damages arising from the
|
data/examples/CQL/OilSupply.cql
CHANGED
@@ -16,7 +16,7 @@ Year Nr is written as Signed Integer(32);
|
|
16
16
|
/*
|
17
17
|
* Entity Types
|
18
18
|
*/
|
19
|
-
Month is identified by its Nr restricted to {1..12};
|
19
|
+
Month [static] is identified by its Nr restricted to {1..12};
|
20
20
|
Month is in one Season;
|
21
21
|
|
22
22
|
Product is independent identified by its Name;
|
@@ -36,7 +36,7 @@ Acceptable Substitution is where
|
|
36
36
|
Product may be substituted by alternate-Product in Season [acyclic, intransitive],
|
37
37
|
alternate-Product is an acceptable substitute for Product in Season;
|
38
38
|
|
39
|
-
Supply Period is identified by Year and Month where
|
39
|
+
Supply Period [separate, static] is identified by Year and Month where
|
40
40
|
Supply Period is in one Year,
|
41
41
|
Supply Period is in one Month;
|
42
42
|
|
@@ -283,9 +283,24 @@ module ActiveFacts
|
|
283
283
|
}
|
284
284
|
end
|
285
285
|
|
286
|
+
rule role_quantifier
|
287
|
+
quantifier mapping_pragmas enforcement cn:context_note?
|
288
|
+
{
|
289
|
+
def ast
|
290
|
+
Compiler::Quantifier.new(
|
291
|
+
quantifier.value[0],
|
292
|
+
quantifier.value[1],
|
293
|
+
enforcement.ast,
|
294
|
+
cn.empty? ? nil : cn.ast,
|
295
|
+
mapping_pragmas.value
|
296
|
+
)
|
297
|
+
end
|
298
|
+
}
|
299
|
+
end
|
300
|
+
|
286
301
|
# This is the rule that causes most back-tracking. I think you can see why.
|
287
302
|
rule simple_role
|
288
|
-
q:
|
303
|
+
q:role_quantifier?
|
289
304
|
player:derived_variable
|
290
305
|
lr:(
|
291
306
|
literal u:unit?
|
@@ -296,12 +311,7 @@ module ActiveFacts
|
|
296
311
|
{
|
297
312
|
def ast
|
298
313
|
if !q.empty? && q.quantifier.value
|
299
|
-
quantifier =
|
300
|
-
q.quantifier.value[0],
|
301
|
-
q.quantifier.value[1],
|
302
|
-
q.enforcement.ast,
|
303
|
-
q.cn.empty? ? nil : q.cn.ast
|
304
|
-
)
|
314
|
+
quantifier = q.ast
|
305
315
|
end
|
306
316
|
if !lr.empty?
|
307
317
|
if lr.respond_to?(:literal)
|
@@ -167,25 +167,31 @@ module ActiveFacts
|
|
167
167
|
|
168
168
|
rule mapping_pragmas
|
169
169
|
'[' s h:mapping_pragma t:(s ',' s mapping_pragma)* s ']' s
|
170
|
-
{
|
170
|
+
{
|
171
|
+
def value
|
172
|
+
t.elements.inject([h.value*' ']) do |a, e|
|
173
|
+
a << e.mapping_pragma.value*' '
|
174
|
+
end
|
175
|
+
end
|
176
|
+
}
|
171
177
|
/
|
172
178
|
s
|
173
179
|
{ def value; []; end }
|
174
180
|
end
|
175
181
|
|
182
|
+
# Each mapping_pragma returns an array of words
|
176
183
|
rule mapping_pragma
|
177
184
|
was s names:(id s)+
|
178
185
|
{ # Old or previous name of an object type:
|
179
186
|
def value
|
180
|
-
[ was.text_value
|
187
|
+
[ was.text_value ] + names.elements.map{|n|n.text_value}
|
181
188
|
end
|
182
189
|
}
|
183
190
|
/
|
184
191
|
head:id tail:(s id)*
|
185
192
|
{ # A sequence of one or more words denoting a pragma:
|
186
193
|
def value
|
187
|
-
([head]+tail.elements.map(&:id)).
|
188
|
-
map(&:text_value)*' '
|
194
|
+
([head]+tail.elements.map(&:id)).map(&:text_value)
|
189
195
|
end
|
190
196
|
}
|
191
197
|
end
|
@@ -1079,6 +1079,11 @@ module ActiveFacts
|
|
1079
1079
|
:max_frequency => @quantifier.max,
|
1080
1080
|
:min_frequency => @quantifier.min
|
1081
1081
|
)
|
1082
|
+
if @quantifier.pragmas
|
1083
|
+
@quantifier.pragmas.each do |p|
|
1084
|
+
constellation.ConceptAnnotation(:concept => constraint.concept, :mapping_annotation => p)
|
1085
|
+
end
|
1086
|
+
end
|
1082
1087
|
trace :constraint, "Made new embedded PC GUID=#{constraint.concept.guid} min=#{@quantifier.min.inspect} max=#{@quantifier.max.inspect} over #{(e = fact_type.entity_type) ? e.name : role_sequence.describe} in #{fact_type.describe}"
|
1083
1088
|
@quantifier.enforcement.compile(constellation, constraint) if @quantifier.enforcement
|
1084
1089
|
@embedded_presence_constraint = constraint
|
@@ -1097,13 +1102,15 @@ module ActiveFacts
|
|
1097
1102
|
class Quantifier
|
1098
1103
|
attr_accessor :enforcement
|
1099
1104
|
attr_accessor :context_note
|
1105
|
+
attr_accessor :pragmas
|
1100
1106
|
attr_reader :min, :max
|
1101
1107
|
|
1102
|
-
def initialize min, max, enforcement = nil, context_note = nil
|
1108
|
+
def initialize min, max, enforcement = nil, context_note = nil, pragmas = nil
|
1103
1109
|
@min = min
|
1104
1110
|
@max = max
|
1105
1111
|
@enforcement = enforcement
|
1106
1112
|
@context_note = context_note
|
1113
|
+
@pragmas = pragmas
|
1107
1114
|
end
|
1108
1115
|
|
1109
1116
|
def is_unique
|
@@ -233,6 +233,11 @@ module ActiveFacts
|
|
233
233
|
:is_preferred_identifier => false,
|
234
234
|
:is_mandatory => @quantifier.min && @quantifier.min > 0
|
235
235
|
)
|
236
|
+
if @quantifier.pragmas
|
237
|
+
@quantifier.pragmas.each do |p|
|
238
|
+
@constellation.ConceptAnnotation(:concept => @constraint.concept, :mapping_annotation => p)
|
239
|
+
end
|
240
|
+
end
|
236
241
|
@enforcement.compile(@constellation, @constraint) if @enforcement
|
237
242
|
trace :constraint, "Made new PC GUID=#{@constraint.concept.guid} min=#{@quantifier.min.inspect} max=#{@quantifier.max.inspect} over #{role_sequence.describe}"
|
238
243
|
super
|
@@ -385,6 +390,11 @@ module ActiveFacts
|
|
385
390
|
:vocabulary => @vocabulary,
|
386
391
|
:is_mandatory => @quantifier.min == 1
|
387
392
|
)
|
393
|
+
if @quantifier.pragmas
|
394
|
+
@quantifier.pragmas.each do |p|
|
395
|
+
@constellation.ConceptAnnotation(:concept => @constraint.concept, :mapping_annotation => p)
|
396
|
+
end
|
397
|
+
end
|
388
398
|
@enforcement.compile(@constellation, @constraint) if @enforcement
|
389
399
|
role_sequences.each_with_index do |role_sequence, i|
|
390
400
|
@constellation.SetComparisonRoles(@constraint, i, :role_sequence => role_sequence)
|
@@ -171,7 +171,7 @@ module ActiveFacts
|
|
171
171
|
end
|
172
172
|
end
|
173
173
|
@pragmas.each do |p|
|
174
|
-
@constellation.ConceptAnnotation(:concept => @fact_type.concept, :mapping_annotation => p)
|
174
|
+
@constellation.ConceptAnnotation(:concept => (@entity_type||@fact_type).concept, :mapping_annotation => p)
|
175
175
|
end if @pragmas
|
176
176
|
|
177
177
|
@clauses.each do |clause|
|
@@ -0,0 +1,241 @@
|
|
1
|
+
#
|
2
|
+
# ActiveFacts Schema Transform
|
3
|
+
# Transform a loaded ActiveFacts vocabulary to suit ActiveRecord
|
4
|
+
#
|
5
|
+
# Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
|
6
|
+
#
|
7
|
+
require 'activefacts/generate/helpers/inject'
|
8
|
+
|
9
|
+
module ActiveFacts
|
10
|
+
module Generate
|
11
|
+
module DataVaultTraits
|
12
|
+
|
13
|
+
module ObjectType
|
14
|
+
|
15
|
+
def dv_add_surrogate type_name = 'Auto Counter', suffix = 'ID'
|
16
|
+
# Find or assert the surrogate value type
|
17
|
+
auto_counter = vocabulary.valid_value_type_name(type_name) ||
|
18
|
+
constellation.ValueType(:vocabulary => vocabulary, :name => type_name, :concept => :new)
|
19
|
+
|
20
|
+
# Create a subtype to identify this entity type:
|
21
|
+
vt_name = self.name + ' '+suffix
|
22
|
+
my_id = @vocabulary.valid_value_type_name(vt_name) ||
|
23
|
+
constellation.ValueType(:vocabulary => vocabulary, :name => vt_name, :concept => :new, :supertype => auto_counter)
|
24
|
+
|
25
|
+
# Create a fact type
|
26
|
+
identifying_fact_type = constellation.FactType(:concept => :new)
|
27
|
+
my_role = constellation.Role(:concept => :new, :fact_type => identifying_fact_type, :ordinal => 0, :object_type => self)
|
28
|
+
self.injected_surrogate_role = my_role
|
29
|
+
id_role = constellation.Role(:concept => :new, :fact_type => identifying_fact_type, :ordinal => 1, :object_type => my_id)
|
30
|
+
|
31
|
+
# Create a reading (which needs a RoleSequence)
|
32
|
+
reading = constellation.Reading(
|
33
|
+
:fact_type => identifying_fact_type,
|
34
|
+
:ordinal => 0,
|
35
|
+
:role_sequence => [:new],
|
36
|
+
:text => "{0} has {1}"
|
37
|
+
)
|
38
|
+
constellation.RoleRef(:role_sequence => reading.role_sequence, :ordinal => 0, :role => my_role)
|
39
|
+
constellation.RoleRef(:role_sequence => reading.role_sequence, :ordinal => 1, :role => id_role)
|
40
|
+
|
41
|
+
# Create two uniqueness constraints for the one-to-one. Each needs a RoleSequence (two RoleRefs)
|
42
|
+
one_id = constellation.PresenceConstraint(
|
43
|
+
:concept => :new,
|
44
|
+
:vocabulary => vocabulary,
|
45
|
+
:name => self.name+'HasOne'+suffix,
|
46
|
+
:role_sequence => [:new],
|
47
|
+
:is_mandatory => true,
|
48
|
+
:min_frequency => 1,
|
49
|
+
:max_frequency => 1,
|
50
|
+
:is_preferred_identifier => false
|
51
|
+
)
|
52
|
+
@constellation.RoleRef(:role_sequence => one_id.role_sequence, :ordinal => 0, :role => my_role)
|
53
|
+
|
54
|
+
one_me = constellation.PresenceConstraint(
|
55
|
+
:concept => :new,
|
56
|
+
:vocabulary => vocabulary,
|
57
|
+
:name => self.name+suffix+'IsOfOne'+self.name,
|
58
|
+
:role_sequence => [:new],
|
59
|
+
:is_mandatory => false,
|
60
|
+
:min_frequency => 0,
|
61
|
+
:max_frequency => 1,
|
62
|
+
:is_preferred_identifier => true
|
63
|
+
)
|
64
|
+
@constellation.RoleRef(:role_sequence => one_me.role_sequence, :ordinal => 0, :role => id_role)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
module ValueType
|
69
|
+
def dv_needs_surrogate
|
70
|
+
!is_auto_assigned
|
71
|
+
end
|
72
|
+
|
73
|
+
def dv_inject_surrogate
|
74
|
+
trace :transform_surrogate, "Adding surrogate ID to Value Type #{name}"
|
75
|
+
add_surrogate('Auto Counter', 'ID')
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
module EntityType
|
80
|
+
def dv_identifying_refs_from
|
81
|
+
pi = preferred_identifier
|
82
|
+
rrs = pi.role_sequence.all_role_ref
|
83
|
+
|
84
|
+
# REVISIT: This is actually a ref to us, not from
|
85
|
+
# if absorbed_via
|
86
|
+
# return [absorbed_via]
|
87
|
+
# end
|
88
|
+
|
89
|
+
rrs.map do |rr|
|
90
|
+
r = references_from.detect{|ref| rr.role == ref.to_role }
|
91
|
+
unless r
|
92
|
+
debugger
|
93
|
+
raise "failed to find #{name} identifying reference for #{rr.role.object_type.name} in #{references_from.inspect}"
|
94
|
+
end
|
95
|
+
r
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def dv_needs_surrogate
|
100
|
+
|
101
|
+
# A recursive proc to replace any reference to an Entity Type by its identifying references:
|
102
|
+
trace :transform_surrogate_expansion, "Expanding key for #{name}"
|
103
|
+
substitute_identifying_refs = proc do |object|
|
104
|
+
if ref = object.absorbed_via
|
105
|
+
# This shouldn't be necessary, but see the absorbed_via comment above.
|
106
|
+
absorbed_into = ref.from
|
107
|
+
trace :transform_surrogate_expansion, "recursing to handle absorption of #{object.name} into #{absorbed_into.name}"
|
108
|
+
[substitute_identifying_refs.call(absorbed_into)]
|
109
|
+
else
|
110
|
+
irf = object.dv_identifying_refs_from
|
111
|
+
trace :transform_surrogate_expansion, "Iterating for #{object.name} over #{irf.inspect}" do
|
112
|
+
irf.each_with_index do |ref, i|
|
113
|
+
next if ref.is_unary
|
114
|
+
next if ref.to_role.object_type.kind_of?(ActiveFacts::Metamodel::ValueType)
|
115
|
+
recurse_to = ref.to_role.object_type
|
116
|
+
|
117
|
+
trace :transform_surrogate_expansion, "#{i}: recursing to expand #{recurse_to.name} key in #{ref}" do
|
118
|
+
irf[i] = substitute_identifying_refs.call(recurse_to)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
irf
|
123
|
+
end
|
124
|
+
end
|
125
|
+
irf = substitute_identifying_refs.call(self)
|
126
|
+
|
127
|
+
trace :transform_surrogate, "Does #{name} need a surrogate? it's identified by #{irf.inspect}" do
|
128
|
+
|
129
|
+
pk_fks = dv_identifying_refs_from.map do |ref|
|
130
|
+
ref.to && ref.to.is_table ? ref.to : nil
|
131
|
+
end
|
132
|
+
|
133
|
+
irf.flatten!
|
134
|
+
|
135
|
+
# Multi-part identifiers are only allowed if:
|
136
|
+
# * each part is a foreign key (i.e. it's a join table),
|
137
|
+
# * there are no other columns (that might require updating) and
|
138
|
+
# * the object is not the target of a foreign key:
|
139
|
+
if irf.size >= 2
|
140
|
+
if pk_fks.include?(nil)
|
141
|
+
trace :transform_surrogate, "#{self.name} needs a surrogate because its multi-part key contains a non-table"
|
142
|
+
return true
|
143
|
+
elsif references_to.size != 0
|
144
|
+
trace :transform_surrogate, "#{self.name} is a join table between #{pk_fks.map(&:name).inspect} but is also an FK target"
|
145
|
+
return true
|
146
|
+
elsif (references_from-dv_identifying_refs_from).size > 0
|
147
|
+
# There are other attributes to worry about
|
148
|
+
return true
|
149
|
+
else
|
150
|
+
trace :transform_surrogate, "#{self.name} is a join table between #{pk_fks.map(&:name).inspect}"
|
151
|
+
return false
|
152
|
+
end
|
153
|
+
return true
|
154
|
+
end
|
155
|
+
|
156
|
+
# Single-part key. It must be an Auto Counter, or we will add a surrogate
|
157
|
+
|
158
|
+
identifying_type = irf[0].to
|
159
|
+
if identifying_type.dv_needs_surrogate
|
160
|
+
trace :transform_surrogate, "#{self.name} needs a surrogate because #{irf[0].to.name} is not an AutoCounter, but #{identifying_type.supertypes_transitive.map(&:name).inspect}"
|
161
|
+
return true
|
162
|
+
end
|
163
|
+
|
164
|
+
false
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def dv_inject_surrogate
|
169
|
+
trace :transform_surrogate, "Injecting a surrogate key into #{self.name}"
|
170
|
+
|
171
|
+
# Disable the preferred identifier:
|
172
|
+
pi = preferred_identifier
|
173
|
+
trace :transform_surrogate, "pi for #{name} was '#{pi.describe}'"
|
174
|
+
pi.is_preferred_identifier = false
|
175
|
+
@preferred_identifier = nil # Kill the cache
|
176
|
+
|
177
|
+
dv_add_surrogate
|
178
|
+
|
179
|
+
trace :transform_surrogate, "pi for #{name} is now '#{preferred_identifier.describe}'"
|
180
|
+
end
|
181
|
+
|
182
|
+
def dv_add_surrogate type_name = 'Auto Counter', suffix = 'ID'
|
183
|
+
# Find or assert the surrogate value type
|
184
|
+
auto_counter = vocabulary.valid_value_type_name(type_name) ||
|
185
|
+
constellation.ValueType(:vocabulary => vocabulary, :name => type_name, :concept => :new)
|
186
|
+
|
187
|
+
# Create a subtype to identify this entity type:
|
188
|
+
vt_name = self.name + ' '+suffix
|
189
|
+
my_id = @vocabulary.valid_value_type_name(vt_name) ||
|
190
|
+
constellation.ValueType(:vocabulary => vocabulary, :name => vt_name, :concept => :new, :supertype => auto_counter)
|
191
|
+
|
192
|
+
# Create a fact type
|
193
|
+
identifying_fact_type = constellation.FactType(:concept => :new)
|
194
|
+
my_role = constellation.Role(:concept => :new, :fact_type => identifying_fact_type, :ordinal => 0, :object_type => self)
|
195
|
+
@injected_surrogate_role = my_role
|
196
|
+
id_role = constellation.Role(:concept => :new, :fact_type => identifying_fact_type, :ordinal => 1, :object_type => my_id)
|
197
|
+
|
198
|
+
# Create a reading (which needs a RoleSequence)
|
199
|
+
reading = constellation.Reading(
|
200
|
+
:fact_type => identifying_fact_type,
|
201
|
+
:ordinal => 0,
|
202
|
+
:role_sequence => [:new],
|
203
|
+
:text => "{0} has {1}"
|
204
|
+
)
|
205
|
+
constellation.RoleRef(:role_sequence => reading.role_sequence, :ordinal => 0, :role => my_role)
|
206
|
+
constellation.RoleRef(:role_sequence => reading.role_sequence, :ordinal => 1, :role => id_role)
|
207
|
+
|
208
|
+
# Create two uniqueness constraints for the one-to-one. Each needs a RoleSequence (two RoleRefs)
|
209
|
+
one_id = constellation.PresenceConstraint(
|
210
|
+
:concept => :new,
|
211
|
+
:vocabulary => vocabulary,
|
212
|
+
:name => self.name+'HasOne'+suffix,
|
213
|
+
:role_sequence => [:new],
|
214
|
+
:is_mandatory => true,
|
215
|
+
:min_frequency => 1,
|
216
|
+
:max_frequency => 1,
|
217
|
+
:is_preferred_identifier => false
|
218
|
+
)
|
219
|
+
@constellation.RoleRef(:role_sequence => one_id.role_sequence, :ordinal => 0, :role => my_role)
|
220
|
+
|
221
|
+
one_me = constellation.PresenceConstraint(
|
222
|
+
:concept => :new,
|
223
|
+
:vocabulary => vocabulary,
|
224
|
+
:name => self.name+suffix+'IsOfOne'+self.name,
|
225
|
+
:role_sequence => [:new],
|
226
|
+
:is_mandatory => false,
|
227
|
+
:min_frequency => 0,
|
228
|
+
:max_frequency => 1,
|
229
|
+
:is_preferred_identifier => true
|
230
|
+
)
|
231
|
+
@constellation.RoleRef(:role_sequence => one_me.role_sequence, :ordinal => 0, :role => id_role)
|
232
|
+
|
233
|
+
return my_id
|
234
|
+
end
|
235
|
+
|
236
|
+
end
|
237
|
+
|
238
|
+
include ActiveFacts::TraitInjector # Must be last in this module, after all submodules have been defined
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
@@ -0,0 +1,266 @@
|
|
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/vocabulary'
|
8
|
+
require 'activefacts/persistence'
|
9
|
+
|
10
|
+
require 'activefacts/generate/traits/datavault'
|
11
|
+
|
12
|
+
module ActiveFacts
|
13
|
+
|
14
|
+
module Generate #:nodoc:
|
15
|
+
module Transform #:nodoc:
|
16
|
+
class DataVault
|
17
|
+
def initialize(vocabulary, *options)
|
18
|
+
@vocabulary = vocabulary
|
19
|
+
@constellation = vocabulary.constellation
|
20
|
+
end
|
21
|
+
|
22
|
+
def classify_tables
|
23
|
+
initial_tables = @vocabulary.tables
|
24
|
+
non_reference_tables = initial_tables.reject do |table|
|
25
|
+
table.concept.all_concept_annotation.detect{|ca| ca.mapping_annotation == 'static'} or
|
26
|
+
!table.is_a?(ActiveFacts::Metamodel::EntityType)
|
27
|
+
end
|
28
|
+
@reference_tables = initial_tables-non_reference_tables
|
29
|
+
|
30
|
+
@link_tables, @hub_tables = non_reference_tables.partition do |table|
|
31
|
+
identifying_references = table.identifier_columns.map{|c| c.references.first}.uniq
|
32
|
+
# Which identifying_references are played by other tables?
|
33
|
+
ir_tables =
|
34
|
+
identifying_references.select do |r|
|
35
|
+
table_referred_to = r.to
|
36
|
+
# I have no examples of multi-level absorption, but it's possible, so loop
|
37
|
+
while av = table_referred_to.absorbed_via
|
38
|
+
table_referred_to = av.from
|
39
|
+
end
|
40
|
+
table_referred_to.is_table
|
41
|
+
end
|
42
|
+
ir_tables.size > 1
|
43
|
+
end
|
44
|
+
trace_table_classifications
|
45
|
+
end
|
46
|
+
|
47
|
+
def trace_table_classifications
|
48
|
+
# Trace the decisions about table types:
|
49
|
+
if trace :datavault
|
50
|
+
[@reference_tables, @hub_tables, @link_tables].zip(['Reference', 'Hub', 'Link']).each do |tables, kind|
|
51
|
+
trace :datavault, kind+' tables: ' do
|
52
|
+
tables.each do |table|
|
53
|
+
identifying_references = table.identifier_columns.map{|c| c.references.first}.uniq
|
54
|
+
trace :datavault, "#{table.name}(#{identifying_references.map{|r| (t = r.to) && t.name || 'self'}*', '})"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def detect_required_surrogates
|
62
|
+
trace :datavault, "Detecting required surrogates" do
|
63
|
+
@required_surrogates =
|
64
|
+
(@hub_tables+@link_tables).select do |table|
|
65
|
+
table.dv_needs_surrogate
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def inject_required_surrogates
|
71
|
+
trace :datavault, "Injecting any required surrogates" do
|
72
|
+
trace :datavault, "Need to inject surrogates into #{@required_surrogates.map(&:name)*', '}"
|
73
|
+
@required_surrogates.each do |table|
|
74
|
+
table.dv_inject_surrogate
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def classify_satellite_references table
|
80
|
+
identifying_references = table.identifier_columns.map{|c| c.references.first}.uniq
|
81
|
+
non_identifying_references = table.columns.map{|c| c.references[0]}.uniq - identifying_references
|
82
|
+
|
83
|
+
# Skip this table if no satellite data is needed
|
84
|
+
# REVISIT: Needed anyway for a link?
|
85
|
+
if non_identifying_references.size == 0
|
86
|
+
return nil
|
87
|
+
end
|
88
|
+
|
89
|
+
satellites = non_identifying_references.inject({}) do |hash, ref|
|
90
|
+
# Extract the declared satellite name, or use just "satellite"
|
91
|
+
satellite_subname =
|
92
|
+
ref.fact_type.internal_presence_constraints.map do |pc|
|
93
|
+
next if !pc.max_frequency || pc.max_frequency > 1 # Not a Uniqueness Constraint
|
94
|
+
next if pc.role_sequence.all_role_ref.size > 1 # Covers more than one role
|
95
|
+
next if pc.role_sequence.all_role_ref.single.role.object_type != table # Not a unique attribute
|
96
|
+
pc.concept.all_concept_annotation.map do |ca|
|
97
|
+
if ca.mapping_annotation =~ /^satellite */
|
98
|
+
ca.mapping_annotation.sub(/^satellite +/, '')
|
99
|
+
else
|
100
|
+
nil
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end.flatten.compact.uniq[0] || "satellite"
|
104
|
+
satellite_name = "#{satellite_subname}"
|
105
|
+
(hash[satellite_name] ||= []) << ref
|
106
|
+
hash
|
107
|
+
end
|
108
|
+
trace :datavault, "#{table.name} satellites are #{satellites.inspect}"
|
109
|
+
satellites
|
110
|
+
end
|
111
|
+
|
112
|
+
def create_one_to_many(one, many, predicate_1 = 'has', predicate_2 = 'is of', one_adj = nil)
|
113
|
+
# Create a fact type
|
114
|
+
fact_type = @constellation.FactType(:concept => :new)
|
115
|
+
one_role = @constellation.Role(:concept => :new, :fact_type => fact_type, :ordinal => 0, :object_type => one)
|
116
|
+
many_role = @constellation.Role(:concept => :new, :fact_type => fact_type, :ordinal => 1, :object_type => many)
|
117
|
+
|
118
|
+
# Create two readings
|
119
|
+
reading2 = @constellation.Reading(:fact_type => fact_type, :ordinal => 0, :role_sequence => [:new], :text => "{0} #{predicate_2} {1}")
|
120
|
+
@constellation.RoleRef(:role_sequence => reading2.role_sequence, :ordinal => 0, :role => many_role)
|
121
|
+
@constellation.RoleRef(:role_sequence => reading2.role_sequence, :ordinal => 1, :role => one_role, :leading_adjective => one_adj)
|
122
|
+
|
123
|
+
reading1 = @constellation.Reading(:fact_type => fact_type, :ordinal => 1, :role_sequence => [:new], :text => "{0} #{predicate_1} {1}")
|
124
|
+
@constellation.RoleRef(:role_sequence => reading1.role_sequence, :ordinal => 0, :role => one_role, :leading_adjective => one_adj)
|
125
|
+
@constellation.RoleRef(:role_sequence => reading1.role_sequence, :ordinal => 1, :role => many_role)
|
126
|
+
|
127
|
+
one_id = @constellation.PresenceConstraint(
|
128
|
+
:concept => :new,
|
129
|
+
:vocabulary => @vocabulary,
|
130
|
+
:name => one.name+'HasOne'+many.name,
|
131
|
+
:role_sequence => [:new],
|
132
|
+
:is_mandatory => true,
|
133
|
+
:min_frequency => 1,
|
134
|
+
:max_frequency => 1,
|
135
|
+
:is_preferred_identifier => false
|
136
|
+
)
|
137
|
+
@constellation.RoleRef(:role_sequence => one_id.role_sequence, :ordinal => 0, :role => many_role)
|
138
|
+
one_role
|
139
|
+
end
|
140
|
+
|
141
|
+
def assert_value_type name, supertype = nil
|
142
|
+
@vocabulary.valid_value_type_name(name) ||
|
143
|
+
@constellation.ValueType(:vocabulary => @vocabulary, :name => name, :supertype => supertype, :concept => :new)
|
144
|
+
end
|
145
|
+
|
146
|
+
def assert_record_source
|
147
|
+
assert_value_type('Record Source', assert_value_type('String'))
|
148
|
+
end
|
149
|
+
|
150
|
+
def assert_date_time
|
151
|
+
assert_value_type('Date Time')
|
152
|
+
end
|
153
|
+
|
154
|
+
# Create a PresenceConstraint with two roles, marked as preferred_identifier
|
155
|
+
def create_two_role_identifier(r1, r2)
|
156
|
+
pc = @constellation.PresenceConstraint(
|
157
|
+
:concept => :new,
|
158
|
+
:vocabulary => @vocabulary,
|
159
|
+
:name => r1.object_type.name+' '+r1.object_type.name+'PK',
|
160
|
+
:role_sequence => [:new],
|
161
|
+
:is_mandatory => true,
|
162
|
+
:min_frequency => 1,
|
163
|
+
:max_frequency => 1,
|
164
|
+
:is_preferred_identifier => true
|
165
|
+
)
|
166
|
+
@constellation.RoleRef(:role_sequence => pc.role_sequence, :ordinal => 0, :role => r1)
|
167
|
+
@constellation.RoleRef(:role_sequence => pc.role_sequence, :ordinal => 1, :role => r2)
|
168
|
+
end
|
169
|
+
|
170
|
+
def create_satellite(table, satellite_name, references)
|
171
|
+
satellite_name = satellite_name.words.titlewords*' '
|
172
|
+
trace :datavault, "Creating #{satellite_name} for #{table.name} with #{references.size} references" do
|
173
|
+
# Create a new entity type with record-date fields in its identifier
|
174
|
+
|
175
|
+
satellite = @constellation.EntityType(:vocabulary => @vocabulary, :name => "#{table.name} #{satellite_name}", :concept => [:new, :implication_rule => "datavault"])
|
176
|
+
satellite.definitely_table
|
177
|
+
|
178
|
+
table_role = create_one_to_many(table, satellite)
|
179
|
+
|
180
|
+
date_time = assert_date_time
|
181
|
+
date_time_role = create_one_to_many(date_time, satellite, 'is of', 'was loaded at', 'load')
|
182
|
+
create_two_role_identifier(table_role, date_time_role)
|
183
|
+
|
184
|
+
record_source = assert_record_source
|
185
|
+
record_source.length = 64
|
186
|
+
record_source_role = create_one_to_many(record_source, satellite, 'is of', 'was loaded from')
|
187
|
+
|
188
|
+
# Move all roles across to it from the parent table.
|
189
|
+
references.each do |ref|
|
190
|
+
trace :datavault, "Moving #{ref} across to #{table.name}_#{satellite_name}" do
|
191
|
+
table_role = ref.fact_type.all_role.detect{|r| r.object_type == table}
|
192
|
+
# Reassign the role player to the satellite:
|
193
|
+
if table_role
|
194
|
+
table_role.object_type = satellite
|
195
|
+
else
|
196
|
+
#debugger # Bum, the crappy Reference object bites again.
|
197
|
+
$stderr.puts "REVISIT: Can't move the role for #{ref.inspect} without mangling the Reference"
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def generate(out = $stdout)
|
205
|
+
@out = out
|
206
|
+
|
207
|
+
# Strategy:
|
208
|
+
# Determine list of ER tables
|
209
|
+
# Partition tables into reference tables (annotated), link tables (two+ FKs in PK), and hub tables
|
210
|
+
# For each hub and link table
|
211
|
+
# Apply a surrogate key if needed (all links, hubs lacking a simple surrogate)
|
212
|
+
# Detect references (fact types) leading to all attributes (non-identifying columns)
|
213
|
+
# Group attribute facts into satellites (use the satellite annotation if present)
|
214
|
+
# For each satellite
|
215
|
+
# Create a new entity type with a (hub-key, record-date key)
|
216
|
+
# Make new one->many fact type between hub and satellite
|
217
|
+
# Modify all attribute facts in this group to attach to the satellite
|
218
|
+
# Compute a gresh relational mapping
|
219
|
+
# Exclude reference tables and disable enforcement to them
|
220
|
+
|
221
|
+
classify_tables
|
222
|
+
|
223
|
+
detect_required_surrogates
|
224
|
+
|
225
|
+
trace :datavault, "Creating satellites" do
|
226
|
+
(@hub_tables+@link_tables).each do |table|
|
227
|
+
satellites = classify_satellite_references table
|
228
|
+
next unless satellites
|
229
|
+
|
230
|
+
trace :datavault, "Creating #{satellites.size} satellites for #{table.name}" do
|
231
|
+
satellites.each do |satellite_name, references|
|
232
|
+
create_satellite(table, satellite_name, references)
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
inject_required_surrogates
|
239
|
+
|
240
|
+
trace :datavault, "Adding standard fields to hubs and links" do
|
241
|
+
(@hub_tables+@link_tables).each do |table|
|
242
|
+
date_time = assert_date_time
|
243
|
+
date_time_role = create_one_to_many(date_time, table, 'is of', 'was loaded at', 'load')
|
244
|
+
|
245
|
+
record_source = assert_record_source
|
246
|
+
record_source_role = create_one_to_many(record_source, table, 'is of', 'was loaded from')
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
# Now, redo the E-R mapping using the revised schema:
|
251
|
+
@vocabulary.decide_tables
|
252
|
+
|
253
|
+
# Before departing, ensure we don't emit the reference tables!
|
254
|
+
@reference_tables.each do |table|
|
255
|
+
table.definitely_not_table
|
256
|
+
@vocabulary.tables.delete(table)
|
257
|
+
end
|
258
|
+
|
259
|
+
end # generate
|
260
|
+
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
ActiveFacts::Registry.generator('transform/datavault', ActiveFacts::Generate::Transform::DataVault)
|
@@ -150,6 +150,7 @@ module ActiveFacts
|
|
150
150
|
(rr = c.role_sequence.all_role_ref.single) and
|
151
151
|
rr.role == self
|
152
152
|
end
|
153
|
+
# REVISIT: check mapping pragmas, e.g. by to_1.concept.all_concept_annotation.detect{|ca| ca.mapping_annotation == 'separate'}
|
153
154
|
|
154
155
|
if fact_type.entity_type
|
155
156
|
# This is a role in an objectified fact type
|
@@ -190,6 +191,7 @@ module ActiveFacts
|
|
190
191
|
def wipe_existing_mapping
|
191
192
|
all_object_type.each do |object_type|
|
192
193
|
object_type.clear_references
|
194
|
+
object_type.wipe_columns
|
193
195
|
object_type.is_table = nil # Undecided; force an attempt to decide
|
194
196
|
object_type.tentative = true # Uncertain
|
195
197
|
end
|
data/lib/activefacts/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activefacts
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Clifford Heath
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-09-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activefacts-api
|
@@ -310,9 +310,11 @@ files:
|
|
310
310
|
- lib/activefacts/generate/stats.rb
|
311
311
|
- lib/activefacts/generate/text.rb
|
312
312
|
- lib/activefacts/generate/topics.rb
|
313
|
+
- lib/activefacts/generate/traits/datavault.rb
|
313
314
|
- lib/activefacts/generate/traits/oo.rb
|
314
315
|
- lib/activefacts/generate/traits/ordered.rb
|
315
316
|
- lib/activefacts/generate/traits/ruby.rb
|
317
|
+
- lib/activefacts/generate/transform/datavault.rb
|
316
318
|
- lib/activefacts/generate/transform/surrogate.rb
|
317
319
|
- lib/activefacts/generate/version.rb
|
318
320
|
- lib/activefacts/input/cql.rb
|