activefacts-compositions 1.9.6 → 1.9.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/Rakefile +33 -0
- data/activefacts-compositions.gemspec +3 -3
- data/bin/schema_compositor +142 -85
- data/lib/activefacts/compositions/binary.rb +19 -15
- data/lib/activefacts/compositions/compositor.rb +126 -125
- data/lib/activefacts/compositions/constraints.rb +74 -54
- data/lib/activefacts/compositions/datavault.rb +545 -0
- data/lib/activefacts/compositions/names.rb +58 -58
- data/lib/activefacts/compositions/relational.rb +801 -692
- data/lib/activefacts/compositions/traits/rails.rb +180 -0
- data/lib/activefacts/compositions/version.rb +1 -1
- data/lib/activefacts/generator/doc/css/ldm.css +45 -0
- data/lib/activefacts/generator/doc/cwm.rb +764 -0
- data/lib/activefacts/generator/doc/glossary.rb +473 -0
- data/lib/activefacts/generator/doc/graphviz.rb +134 -0
- data/lib/activefacts/generator/doc/ldm.rb +698 -0
- data/lib/activefacts/generator/oo.rb +130 -124
- data/lib/activefacts/generator/rails/models.rb +237 -0
- data/lib/activefacts/generator/rails/schema.rb +273 -0
- data/lib/activefacts/generator/ruby.rb +75 -67
- data/lib/activefacts/generator/sql.rb +333 -351
- data/lib/activefacts/generator/sql/server.rb +100 -39
- data/lib/activefacts/generator/summary.rb +67 -59
- data/lib/activefacts/generator/validate.rb +19 -134
- metadata +18 -15
@@ -0,0 +1,134 @@
|
|
1
|
+
#
|
2
|
+
# ActiveFacts GraphViz generator
|
3
|
+
#
|
4
|
+
# Copyright (c) 2009-2016 Clifford Heath. Read the LICENSE file.
|
5
|
+
#
|
6
|
+
require 'activefacts/metamodel'
|
7
|
+
require 'activefacts/registry'
|
8
|
+
require 'activefacts/compositions'
|
9
|
+
require 'activefacts/generator'
|
10
|
+
|
11
|
+
module ActiveFacts
|
12
|
+
module Generators
|
13
|
+
# Options are comma or space separated:
|
14
|
+
# * delay_fks Leave all foreign keys until the end, not just those that contain forward-references
|
15
|
+
# * underscore
|
16
|
+
module Doc
|
17
|
+
class Graphviz
|
18
|
+
def self.options
|
19
|
+
{
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize composition, options = {}
|
24
|
+
@composition = composition
|
25
|
+
@options = options
|
26
|
+
end
|
27
|
+
|
28
|
+
def generate
|
29
|
+
composites = @composition.all_composite.sort_by{|c| c.mapping.name }
|
30
|
+
header +
|
31
|
+
tables(composites) + "\n" +
|
32
|
+
# All-equal ranks causes graphviz to shit itself
|
33
|
+
# ranks(composites) +
|
34
|
+
fks(composites) +
|
35
|
+
footer
|
36
|
+
end
|
37
|
+
|
38
|
+
def header
|
39
|
+
<<-END
|
40
|
+
digraph G {
|
41
|
+
fontname = Helvetica;
|
42
|
+
outputmode = "nodesfirst";
|
43
|
+
XXratio = 1.4; // pad when done to this aspect ratio (portrait)
|
44
|
+
rankdir = LR; // Requires extra { } in record labels
|
45
|
+
|
46
|
+
// You might like neato's layout better than fdp's:
|
47
|
+
// layout = neato; // neato, dot, sfdp, circo
|
48
|
+
// mode = KK;
|
49
|
+
|
50
|
+
graph[
|
51
|
+
layout = fdp; // neato, dot, sfdp, circo
|
52
|
+
overlap = false; // scalexy, compress
|
53
|
+
splines = ortho; // ortho, compound, curved
|
54
|
+
packmode = "graph"; // node, clust, graph, array_c4. Turns on pack=true
|
55
|
+
mclimit = 3.0;
|
56
|
+
sep = 0.2; // Treat nodes as though they were 1.2 times what they actually are
|
57
|
+
// nodesep = 0.6; // in dot, it's inches. others, who knows?
|
58
|
+
];
|
59
|
+
|
60
|
+
node[
|
61
|
+
shape=record,
|
62
|
+
width=.1,
|
63
|
+
height=.1
|
64
|
+
];
|
65
|
+
END
|
66
|
+
end
|
67
|
+
|
68
|
+
def footer
|
69
|
+
"}\n"
|
70
|
+
end
|
71
|
+
|
72
|
+
def tables composites
|
73
|
+
composites.map.with_index(1) do |composite, i|
|
74
|
+
"t#{i}[shape=record, label=\"{#{
|
75
|
+
named_stack(
|
76
|
+
text(composite.mapping.name),
|
77
|
+
[columns(
|
78
|
+
stack(composite.mapping.all_leaf.map{''}),
|
79
|
+
stack(composite.mapping.all_leaf.map.with_index(1){|l, i| tagged_text("c#{i}:e", l.column_name.capcase)})
|
80
|
+
)]
|
81
|
+
)
|
82
|
+
}}\", style=rounded]"
|
83
|
+
end.map{|t| " #{t};\n"}*''
|
84
|
+
end
|
85
|
+
|
86
|
+
def ranks composites
|
87
|
+
" { rank = same; #{(1..composites.size).map{|i| "t#{i}".inspect+'; '}*'' }}\n"
|
88
|
+
end
|
89
|
+
|
90
|
+
def fks composites
|
91
|
+
composites.flat_map.with_index(1) do |composite, cnum|
|
92
|
+
composite.all_foreign_key_as_source_composite.map do |fk|
|
93
|
+
target = fk.composite
|
94
|
+
target_num = composites.index(target)+1
|
95
|
+
fkc = fk.all_foreign_key_field[0].component
|
96
|
+
mandatory = fkc.path_mandatory
|
97
|
+
source_col_num = composite.mapping.all_leaf.index(fkc)+1
|
98
|
+
"t#{target_num}:name:e -> t#{cnum}:c#{source_col_num}:w[arrowhead=invempty#{mandatory ? 'tee' : ''}; arrowsize=2;]"
|
99
|
+
# Also, arrowtail. small circle is
|
100
|
+
# splineType=...
|
101
|
+
end
|
102
|
+
end.
|
103
|
+
map{|f| f && " #{f};\n"}.
|
104
|
+
compact*''
|
105
|
+
end
|
106
|
+
|
107
|
+
def stack items
|
108
|
+
"{#{items*'|'}}"
|
109
|
+
end
|
110
|
+
|
111
|
+
def columns *a
|
112
|
+
stack a
|
113
|
+
end
|
114
|
+
|
115
|
+
def named_stack head, items
|
116
|
+
"{<name>#{head}#{!items.empty? && "|{#{stack items}}" || ''}}"
|
117
|
+
end
|
118
|
+
|
119
|
+
def tagged_text tag, txt
|
120
|
+
"<#{tag}> "+text(txt)
|
121
|
+
end
|
122
|
+
|
123
|
+
def text t
|
124
|
+
t.gsub(/[ |<>]/){|c| "\\#{c}"}
|
125
|
+
end
|
126
|
+
|
127
|
+
MM = ActiveFacts::Metamodel unless const_defined?(:MM)
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
publish_generator Doc::Graphviz
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
@@ -0,0 +1,698 @@
|
|
1
|
+
#
|
2
|
+
# ActiveFacts Logical Data Model Generator
|
3
|
+
#
|
4
|
+
# This generator produces an HTML-formated Logical Data Model of a Vocabulary.
|
5
|
+
#
|
6
|
+
# Copyright (c) 2016 Infinuendo. Read the LICENSE file.
|
7
|
+
#
|
8
|
+
require 'digest/sha1'
|
9
|
+
require 'activefacts/metamodel'
|
10
|
+
require 'activefacts/registry'
|
11
|
+
require 'activefacts/compositions'
|
12
|
+
require 'activefacts/generator'
|
13
|
+
require 'activefacts/support'
|
14
|
+
|
15
|
+
module ActiveFacts
|
16
|
+
module Generators
|
17
|
+
module Doc
|
18
|
+
class LDM
|
19
|
+
def self.options
|
20
|
+
{
|
21
|
+
underscore: [String, "Use 'str' instead of underscore between words in table names"]
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize composition, options = {}
|
26
|
+
@composition = composition
|
27
|
+
@options = options
|
28
|
+
@underscore = options.has_key?("underscore") ? (options['underscore'] || '_') : ''
|
29
|
+
|
30
|
+
@vocabulary = composition.constellation.Vocabulary.values[0] # REVISIT when importing from other vocabularies
|
31
|
+
# glossary_options = {"gen_bootstrap" => true}
|
32
|
+
# @glossary = GLOSSARY.new(@vocabulary, glossary_options)
|
33
|
+
end
|
34
|
+
|
35
|
+
def generate
|
36
|
+
@tables_emitted = {}
|
37
|
+
|
38
|
+
# trace.enable 'ldm'
|
39
|
+
|
40
|
+
generate_header +
|
41
|
+
generate_definitions +
|
42
|
+
generate_diagrams +
|
43
|
+
generate_details +
|
44
|
+
generate_footer
|
45
|
+
end
|
46
|
+
|
47
|
+
def table_name_max
|
48
|
+
60
|
49
|
+
end
|
50
|
+
|
51
|
+
def column_name_max
|
52
|
+
40
|
53
|
+
end
|
54
|
+
|
55
|
+
def index_name_max
|
56
|
+
60
|
57
|
+
end
|
58
|
+
|
59
|
+
def schema_name_max
|
60
|
+
60
|
61
|
+
end
|
62
|
+
|
63
|
+
def safe_table_name composite
|
64
|
+
escape(table_name(composite), table_name_max)
|
65
|
+
end
|
66
|
+
|
67
|
+
def safe_column_name component
|
68
|
+
escape(column_name(component), column_name_max)
|
69
|
+
end
|
70
|
+
|
71
|
+
def table_name composite
|
72
|
+
composite.mapping.name.gsub(' ', @underscore)
|
73
|
+
end
|
74
|
+
|
75
|
+
def column_name component
|
76
|
+
component.column_name.capcase
|
77
|
+
end
|
78
|
+
|
79
|
+
def generate_header
|
80
|
+
css_file = "/css/ldm.css"
|
81
|
+
|
82
|
+
"<!DOCTYPE html>\n" +
|
83
|
+
"<html lang=\"en\">\n" +
|
84
|
+
" <head>\n" +
|
85
|
+
" <meta charset=\"utf-8\">\n" +
|
86
|
+
" <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n" +
|
87
|
+
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n" +
|
88
|
+
" <!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->\n" +
|
89
|
+
" <title>Logical Data Model for " + @composition.name + "</title>\n" +
|
90
|
+
"\n" +
|
91
|
+
" <!-- Bootstrap -->\n" +
|
92
|
+
" <link rel=\"stylesheet\" href=\"https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css\" integrity=\"sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7\" crossorigin=\"anonymous\">\n" +
|
93
|
+
"\n" +
|
94
|
+
" <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->\n" +
|
95
|
+
" <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->\n" +
|
96
|
+
" <!--[if lt IE 9]>\n" +
|
97
|
+
" <script src=\"https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js\"></script>\n" +
|
98
|
+
" <script src=\"https://oss.maxcdn.com/respond/1.4.2/respond.min.js\"></script>\n" +
|
99
|
+
" <![endif]-->\n" +
|
100
|
+
File.open(File.dirname(__FILE__)+css_file) do |f|
|
101
|
+
" <style media='screen' type='text/css'>\n" +
|
102
|
+
f.read + "\n" +
|
103
|
+
" </style>\n"
|
104
|
+
end +
|
105
|
+
" </head>\n" +
|
106
|
+
" <body>\n" +
|
107
|
+
" <div class=\"container\">\n" +
|
108
|
+
" <div class=\"row\">\n" +
|
109
|
+
" <div class=\"col-md-12\">\n" +
|
110
|
+
h1("Logical Data Model for " + @composition.name)
|
111
|
+
end
|
112
|
+
|
113
|
+
def generate_definitions
|
114
|
+
defns =
|
115
|
+
@composition.
|
116
|
+
all_composite.
|
117
|
+
reject {|c| c.mapping.object_type.is_a?(ActiveFacts::Metamodel::ValueType)}.
|
118
|
+
reject {|c| c.mapping.object_type.is_static}.
|
119
|
+
reject {|c| c.mapping.object_type.fact_type}.
|
120
|
+
map {|c| c.mapping.object_type}
|
121
|
+
|
122
|
+
@definitions = {}
|
123
|
+
defns.each do |o|
|
124
|
+
@definitions[o] = true
|
125
|
+
end
|
126
|
+
|
127
|
+
defns.each do |o|
|
128
|
+
ftm = relevant_fact_types(o)
|
129
|
+
|
130
|
+
trace :ldm, "expanding #{o.name}"
|
131
|
+
|
132
|
+
ftm.each do |r, ft|
|
133
|
+
next if ft.is_a?(ActiveFacts::Metamodel::TypeInheritance)
|
134
|
+
ft.all_role.each do |ftr|
|
135
|
+
next if @definitions[ftr.object_type]
|
136
|
+
next if ftr.object_type.is_a?(ActiveFacts::Metamodel::ValueType)
|
137
|
+
|
138
|
+
trace :ldm, "adding #{ftr.object_type.name}"
|
139
|
+
|
140
|
+
defns = defns << ftr.object_type
|
141
|
+
@definitions[ftr.object_type] = true
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
" <h2>Business Definitions and Relationships</h2>\n" +
|
147
|
+
defns.sort_by{|o| o.name.gsub(/ /, '').downcase}.map do |o|
|
148
|
+
entity_type_dump(o, 0)
|
149
|
+
end * "\n" + "\n"
|
150
|
+
end
|
151
|
+
|
152
|
+
def generate_diagrams
|
153
|
+
''
|
154
|
+
end
|
155
|
+
|
156
|
+
def generate_details
|
157
|
+
h2("Logical Data Model Details") +
|
158
|
+
@composition.
|
159
|
+
all_composite.
|
160
|
+
sort_by{|composite| composite.mapping.name}.
|
161
|
+
map{|composite| generate_table(composite)}*"\n" + "\n"
|
162
|
+
end
|
163
|
+
|
164
|
+
def generate_footer
|
165
|
+
" </div>\n" +
|
166
|
+
" </div>\n" +
|
167
|
+
" </div>\n" +
|
168
|
+
" <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->\n" +
|
169
|
+
" <script src=\"https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js\"></script>\n" +
|
170
|
+
" <!-- Include all compiled plugins (below), or include individual files as needed -->\n" +
|
171
|
+
" <script src=\"https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js\" integrity=\"sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS\" crossorigin=\"anonymous\"></script>\n" +
|
172
|
+
# @glossary.glossary_end +
|
173
|
+
" </body>\n" +
|
174
|
+
"</html>\n"
|
175
|
+
end
|
176
|
+
|
177
|
+
#
|
178
|
+
# Standard document elements
|
179
|
+
#
|
180
|
+
def element(text, attrs, tag = 'span')
|
181
|
+
"<#{tag}#{attrs.empty? ? '' : attrs.map{|k,v| " #{k}='#{v}'"}*''}>#{text}</#{tag}>"
|
182
|
+
end
|
183
|
+
|
184
|
+
def span(text, klass = nil)
|
185
|
+
element(text, klass ? {:class => klass} : {})
|
186
|
+
end
|
187
|
+
|
188
|
+
def div(text, klass = nil)
|
189
|
+
element(text, klass ? {:class => klass} : {}, 'div')
|
190
|
+
end
|
191
|
+
|
192
|
+
def h1(text, klass = nil)
|
193
|
+
element(text, klass ? {:class => klass} : {}, 'h1')
|
194
|
+
end
|
195
|
+
|
196
|
+
def h2(text, klass = nil)
|
197
|
+
element(text, klass ? {:class => klass} : {}, 'h2')
|
198
|
+
end
|
199
|
+
|
200
|
+
def h3(text, klass = nil)
|
201
|
+
element(text, klass ? {:class => klass} : {}, 'h3')
|
202
|
+
end
|
203
|
+
|
204
|
+
def dl(text, klass = nil)
|
205
|
+
element(text, klass ? {:class => klass} : {}, 'dl')
|
206
|
+
end
|
207
|
+
|
208
|
+
# A definition of a term
|
209
|
+
def termdef(name)
|
210
|
+
element(name, {:name => name, :class => 'object_type'}, 'a')
|
211
|
+
end
|
212
|
+
|
213
|
+
# A reference to a defined term (excluding role adjectives)
|
214
|
+
def termref(name, role_name = nil, o = nil)
|
215
|
+
if o && !@definitions[o]
|
216
|
+
element(name, :class=>:object_type)
|
217
|
+
else
|
218
|
+
role_name ||= name
|
219
|
+
element(role_name, {:href=>'#'+name, :class=>:object_type}, 'a')
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
# Text that should appear as part of a term (including role adjectives)
|
224
|
+
def term(name)
|
225
|
+
element(name, :class=>:object_type)
|
226
|
+
end
|
227
|
+
|
228
|
+
#
|
229
|
+
# Dump functions
|
230
|
+
#
|
231
|
+
def entity_type_dump(o, level)
|
232
|
+
pi = o.preferred_identifier
|
233
|
+
supers = o.supertypes
|
234
|
+
if (supers.size > 0) # Ignore identification by a supertype:
|
235
|
+
pi = nil if pi && pi.role_sequence.all_role_ref.detect{ |rr|
|
236
|
+
rr.role.fact_type.is_a?(ActiveFacts::Metamodel::TypeInheritance)
|
237
|
+
}
|
238
|
+
end
|
239
|
+
|
240
|
+
cn_array = o.concept.all_context_note_as_relevant_concept.map{|cn| [cn.context_note_kind, cn.discussion] }
|
241
|
+
cn_hash = cn_array.inject({}) do |hash, value|
|
242
|
+
hash[value.first] = value.last
|
243
|
+
hash
|
244
|
+
end
|
245
|
+
|
246
|
+
informal_defn = cn_hash["because"]
|
247
|
+
defn_term =
|
248
|
+
" <div class=\"row\">\n" +
|
249
|
+
" <div class=\"col-md-12 definition\">\n" +
|
250
|
+
" A #{termdef(o.name)} #{informal_defn ? 'is ' + informal_defn : ''}\n" +
|
251
|
+
" </div>\n" +
|
252
|
+
" </div>\n"
|
253
|
+
|
254
|
+
defn_detail =
|
255
|
+
" <div class=\"row\">\n" +
|
256
|
+
" <div class=\"col-md-12 details\">\n" +
|
257
|
+
(supers.size > 0 ?
|
258
|
+
"#{span('Each', 'keyword')} #{termref(o.name, nil, o)} #{span('is a kind of', 'keyword')} #{supers.map{|s| termref(s.name, nil, s)}*', '}\n" :
|
259
|
+
''
|
260
|
+
) +
|
261
|
+
if pi
|
262
|
+
"#{span('Each', 'keyword')} #{termref(o.name, nil, o)} #{span('is identified by', 'keyword')} " +
|
263
|
+
pi.role_sequence.all_role_ref_in_order.map do |rr|
|
264
|
+
termref(
|
265
|
+
rr.role.object_type.name,
|
266
|
+
[ rr.leading_adjective,
|
267
|
+
rr.role.role_name || rr.role.object_type.name,
|
268
|
+
rr.trailing_adjective
|
269
|
+
].compact * '-',
|
270
|
+
rr.role.object_type
|
271
|
+
)
|
272
|
+
end * ", " + "\n"
|
273
|
+
else
|
274
|
+
''
|
275
|
+
end +
|
276
|
+
fact_types_dump(o, relevant_fact_types(o)) + "\n" +
|
277
|
+
" </div>\n" +
|
278
|
+
" </div>\n"
|
279
|
+
|
280
|
+
defn_term + defn_detail
|
281
|
+
end
|
282
|
+
|
283
|
+
def relevant_fact_types(o)
|
284
|
+
o.
|
285
|
+
all_role.
|
286
|
+
map{|r| [r, r.fact_type]}.
|
287
|
+
reject { |r, ft| ft.is_a?(ActiveFacts::Metamodel::LinkFactType) }.
|
288
|
+
select { |r, ft| ft.entity_type || has_another_nonstatic_role(ft, r) }
|
289
|
+
end
|
290
|
+
|
291
|
+
def has_another_nonstatic_role(ft, r)
|
292
|
+
ft.all_role.detect do |rr|
|
293
|
+
rr != r &&
|
294
|
+
rr.object_type.is_a?(ActiveFacts::Metamodel::EntityType) &&
|
295
|
+
!rr.object_type.is_static
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
def fact_types_dump(o, ftm)
|
300
|
+
ftm.
|
301
|
+
map { |r, ft| [ft, " #{fact_type_dump(ft, o)}"] }.
|
302
|
+
sort_by{|ft, text| [ ft.is_a?(ActiveFacts::Metamodel::TypeInheritance) ? 0 : 1, text]}.
|
303
|
+
map{|ft, text| text} * "\n"
|
304
|
+
end
|
305
|
+
|
306
|
+
def fact_type_dump(ft, wrt = nil)
|
307
|
+
if ft.entity_type
|
308
|
+
div(
|
309
|
+
div(span('Each ', 'keyword') + termref(ft.entity_type.name, nil, ft.entity_type) + span(' is where ', 'keyword')) +
|
310
|
+
div(expand_fact_type(ft, wrt, true, 'some')),
|
311
|
+
'glossary-objectification'
|
312
|
+
)
|
313
|
+
else
|
314
|
+
fact_type_block(ft, wrt)
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
def fact_type_block(ft, wrt = nil, include_rolenames = true)
|
319
|
+
div(expand_fact_type(ft, wrt, include_rolenames, ''), 'glossary-facttype')
|
320
|
+
end
|
321
|
+
|
322
|
+
def expand_fact_type(ft, wrt = nil, include_rolenames = true, wrt_qualifier = '')
|
323
|
+
role = ft.all_role.detect{|r| r.object_type == wrt}
|
324
|
+
preferred_reading = ft.reading_preferably_starting_with_role(role)
|
325
|
+
alternate_readings = ft.all_reading.reject{|r| r == preferred_reading}
|
326
|
+
|
327
|
+
div(
|
328
|
+
expand_reading(preferred_reading, include_rolenames, wrt, wrt_qualifier),
|
329
|
+
'glossary-reading'
|
330
|
+
)
|
331
|
+
end
|
332
|
+
|
333
|
+
def role_ref(rr, freq_con, l_adj, name, t_adj, role_name_def, literal)
|
334
|
+
term_parts = [l_adj, termref(name, nil, rr.role.object_type), t_adj].compact
|
335
|
+
[
|
336
|
+
freq_con ? element(freq_con, :class=>:keyword) : nil,
|
337
|
+
term_parts.size > 1 ? term([l_adj, termref(name, nil, rr.role.object_type), t_adj].compact*' ') : term_parts[0],
|
338
|
+
role_name_def,
|
339
|
+
literal
|
340
|
+
]
|
341
|
+
end
|
342
|
+
|
343
|
+
def expand_reading(reading, include_rolenames = true, wrt = nil, wrt_qualifier = '')
|
344
|
+
role_refs = reading.role_sequence.all_role_ref.sort_by{|role_ref| role_ref.ordinal}
|
345
|
+
lrr = role_refs[role_refs.size - 1]
|
346
|
+
element(
|
347
|
+
# element(rr.role.is_unique ? "one" : "some", :class=>:keyword) +
|
348
|
+
reading.expand([], include_rolenames) do |rr, freq_con, l_adj, name, t_adj, role_name_def, literal|
|
349
|
+
if role_name_def
|
350
|
+
role_name_def = role_name_def.gsub(/\(as ([^)]+)\)/) {
|
351
|
+
span("(as #{ termref(rr.role.object_type.name, $1, rr.role.object_type) })", 'keyword')
|
352
|
+
}
|
353
|
+
end
|
354
|
+
# qualify the last role of the reading
|
355
|
+
quantifier = ''
|
356
|
+
if rr == lrr
|
357
|
+
uniq = true
|
358
|
+
(0 ... role_refs.size - 2).each{|i| uniq = uniq && role_refs[i].role.is_unique }
|
359
|
+
quantifier = uniq ? "one" : "at least one"
|
360
|
+
end
|
361
|
+
role_ref(rr, quantifier, l_adj, name, t_adj, role_name_def, literal)
|
362
|
+
end,
|
363
|
+
{:class => 'reading'}
|
364
|
+
)
|
365
|
+
end
|
366
|
+
|
367
|
+
def generate_table(composite)
|
368
|
+
@tables_emitted[composite] = true
|
369
|
+
delayed_indices = []
|
370
|
+
|
371
|
+
table_defn =
|
372
|
+
" <h3 id=\"LDMD_#{table_name(composite)}\">#{composite.mapping.name}</h3>\n" +
|
373
|
+
" <table class=\"table table-bordered table-striped\">\n" +
|
374
|
+
" <thead style=\"background-color: #aaa;\">\n" +
|
375
|
+
" <tr>\n" +
|
376
|
+
" <th>Attribute</th><th>Data Type</th><th>Man</th><th>Description</th>\n" +
|
377
|
+
" </tr>\n" +
|
378
|
+
" </thead>\n" +
|
379
|
+
" <tbody>\n" +
|
380
|
+
(
|
381
|
+
composite.mapping.all_leaf.flat_map do |leaf|
|
382
|
+
# Absorbed empty subtypes appear as leaves
|
383
|
+
next if leaf.is_a?(MM::Absorption) && leaf.parent_role.fact_type.is_a?(MM::TypeInheritance)
|
384
|
+
|
385
|
+
generate_column leaf, 11
|
386
|
+
end
|
387
|
+
).compact.flat_map{|f| "#{f}" }*"\n"+"\n" +
|
388
|
+
" </tbody>\n" +
|
389
|
+
" </table>\n"
|
390
|
+
|
391
|
+
table_keys =
|
392
|
+
(
|
393
|
+
composite.all_index.map do |index|
|
394
|
+
generate_index index, delayed_indices, 9
|
395
|
+
end.compact.sort +
|
396
|
+
composite.all_foreign_key_as_source_composite.map do |fk|
|
397
|
+
# trace :ldm, "generate foreign key for #{fk.composite.mapping.name}"
|
398
|
+
generate_foreign_key fk, 9
|
399
|
+
end.compact.sort
|
400
|
+
).compact.flat_map{|f| "#{f}" }*"<br>\n"+"\n"
|
401
|
+
|
402
|
+
table_values =
|
403
|
+
if composite.mapping.object_type.all_instance.size > 0 then
|
404
|
+
table_values =
|
405
|
+
" <table class=\"table table-bordered table-striped\">\n" +
|
406
|
+
" <thead style=\"background-color: #aaa;\">\n" +
|
407
|
+
" <tr>\n" +
|
408
|
+
(
|
409
|
+
composite.mapping.all_leaf.flat_map do |leaf|
|
410
|
+
# Absorbed empty subtypes appear as leaves
|
411
|
+
next if leaf.is_a?(MM::Absorption) && leaf.parent_role.fact_type.is_a?(MM::TypeInheritance)
|
412
|
+
column_name = safe_column_name(leaf)
|
413
|
+
" " * 11 + " <th>#{column_name}\n"
|
414
|
+
end
|
415
|
+
) * "\n" + "\n" +
|
416
|
+
" </tr>\n" +
|
417
|
+
" </thead>\n" +
|
418
|
+
" <tbody>\n" +
|
419
|
+
" </tbody>\n" +
|
420
|
+
" </table>\n"
|
421
|
+
else
|
422
|
+
''
|
423
|
+
end
|
424
|
+
|
425
|
+
table_defn + table_keys + table_values
|
426
|
+
end
|
427
|
+
|
428
|
+
def generate_column leaf, indent
|
429
|
+
column_name = safe_column_name(leaf)
|
430
|
+
padding = " "*(column_name.size >= column_name_max ? 1 : column_name_max-column_name.size)
|
431
|
+
constraints = leaf.all_leaf_constraint
|
432
|
+
|
433
|
+
identity = ''
|
434
|
+
|
435
|
+
" " * indent + "<tr>\n" +
|
436
|
+
" " * indent + " <td>#{column_name}\n" +
|
437
|
+
" " * indent + " <td>#{component_type(leaf, column_name)}\n" +
|
438
|
+
" " * indent + " <td>#{leaf.path_mandatory ? 'Yes' : 'No'}\n" +
|
439
|
+
" " * indent + " <td>#{column_comment leaf}\n" +
|
440
|
+
" " * indent + "</tr>"
|
441
|
+
# "-- #{column_comment leaf}\n\t#{column_name}#{padding}#{component_type leaf, column_name}#{identity}"
|
442
|
+
end
|
443
|
+
|
444
|
+
def column_comment component
|
445
|
+
return '' unless cp = component.parent
|
446
|
+
prefix = column_comment(cp)
|
447
|
+
name = component.name
|
448
|
+
if component.is_a?(MM::Absorption)
|
449
|
+
reading = component.parent_role.fact_type.reading_preferably_starting_with_role(component.parent_role).expand([], false)
|
450
|
+
maybe = component.parent_role.is_mandatory ? '' : 'maybe '
|
451
|
+
cpname = cp.name
|
452
|
+
if prefix[(-cpname.size-1)..-1] == ' '+cpname && reading[0..cpname.size] == cpname+' '
|
453
|
+
prefix+' that ' + maybe + reading[cpname.size+1..-1]
|
454
|
+
else
|
455
|
+
(prefix.empty? ? '' : prefix+' and ') + maybe + reading
|
456
|
+
end
|
457
|
+
else
|
458
|
+
name
|
459
|
+
end
|
460
|
+
end
|
461
|
+
|
462
|
+
def boolean_type
|
463
|
+
'boolean'
|
464
|
+
end
|
465
|
+
|
466
|
+
def surrogate_type
|
467
|
+
'bigint'
|
468
|
+
end
|
469
|
+
|
470
|
+
def component_type component, column_name
|
471
|
+
case component
|
472
|
+
when MM::Indicator
|
473
|
+
boolean_type
|
474
|
+
when MM::SurrogateKey
|
475
|
+
surrogate_type
|
476
|
+
when MM::ValueField, MM::Absorption
|
477
|
+
object_type = component.object_type
|
478
|
+
while object_type.is_a?(MM::EntityType)
|
479
|
+
rr = object_type.preferred_identifier.role_sequence.all_role_ref.single
|
480
|
+
raise "Can't produce a column for composite #{component.inspect}" unless rr
|
481
|
+
object_type = rr.role.object_type
|
482
|
+
end
|
483
|
+
raise "A column can only be produced from a ValueType" unless object_type.is_a?(MM::ValueType)
|
484
|
+
|
485
|
+
if component.is_a?(MM::Absorption)
|
486
|
+
value_constraint ||= component.child_role.role_value_constraint
|
487
|
+
end
|
488
|
+
|
489
|
+
supertype = object_type
|
490
|
+
begin
|
491
|
+
object_type = supertype
|
492
|
+
length ||= object_type.length
|
493
|
+
scale ||= object_type.scale
|
494
|
+
unless component.parent.parent and component.parent.foreign_key
|
495
|
+
# No need to enforce value constraints that are already enforced by a foreign key
|
496
|
+
value_constraint ||= object_type.value_constraint
|
497
|
+
end
|
498
|
+
end while supertype = object_type.supertype
|
499
|
+
type, length = normalise_type(object_type.name, length)
|
500
|
+
sql_type = "#{type}#{
|
501
|
+
if !length
|
502
|
+
''
|
503
|
+
else
|
504
|
+
'(' + length.to_s + (scale ? ", #{scale}" : '') + ')'
|
505
|
+
end
|
506
|
+
# }#{
|
507
|
+
# (component.path_mandatory ? '' : ' NOT') + ' NULL'
|
508
|
+
# }#{
|
509
|
+
# # REVISIT: This is an SQL Server-ism. Replace with a standard SQL SEQUENCE/
|
510
|
+
# # Emit IDENTITY for columns auto-assigned on commit (except FKs)
|
511
|
+
# if a = object_type.is_auto_assigned and a != 'assert' and
|
512
|
+
# !component.all_foreign_key_field.detect{|fkf| fkf.foreign_key.source_composite == component.root}
|
513
|
+
# ' IDENTITY'
|
514
|
+
# else
|
515
|
+
# ''
|
516
|
+
# end
|
517
|
+
}#{
|
518
|
+
value_constraint ? check_clause(column_name, value_constraint) : ''
|
519
|
+
}"
|
520
|
+
when MM::Injection
|
521
|
+
component.object_type.name
|
522
|
+
else
|
523
|
+
raise "Can't make a column from #{component}"
|
524
|
+
end
|
525
|
+
end
|
526
|
+
|
527
|
+
def generate_index index, delayed_indices, indent
|
528
|
+
nullable_columns =
|
529
|
+
index.all_index_field.select do |ixf|
|
530
|
+
!ixf.component.path_mandatory
|
531
|
+
end
|
532
|
+
contains_nullable_columns = nullable_columns.size > 0
|
533
|
+
|
534
|
+
primary = index.composite_as_primary_index && !contains_nullable_columns
|
535
|
+
column_names =
|
536
|
+
index.all_index_field.map do |ixf|
|
537
|
+
column_name(ixf.component)
|
538
|
+
end
|
539
|
+
clustering =
|
540
|
+
(index.composite_as_primary_index ? ' CLUSTERED' : ' NONCLUSTERED')
|
541
|
+
|
542
|
+
if contains_nullable_columns
|
543
|
+
table_name = safe_table_name(index.composite)
|
544
|
+
delayed_indices <<
|
545
|
+
'CREATE UNIQUE'+clustering+' INDEX '+
|
546
|
+
escape("#{table_name(index.composite)}By#{column_names*''}", index_name_max) +
|
547
|
+
" ON #{table_name}("+column_names.map{|n| escape(n, column_name_max)}*', ' +
|
548
|
+
") WHERE #{
|
549
|
+
nullable_columns.
|
550
|
+
map{|ixf| safe_column_name ixf.component}.
|
551
|
+
map{|column_name| column_name + ' IS NOT NULL'} *
|
552
|
+
' AND '
|
553
|
+
}"
|
554
|
+
nil
|
555
|
+
else
|
556
|
+
# '-- '+index.inspect
|
557
|
+
" " * indent + (primary ? 'PRIMARY KEY' : 'UNIQUE') +
|
558
|
+
clustering +
|
559
|
+
"(#{column_names.map{|n| escape(n, column_name_max)}*', '})"
|
560
|
+
end
|
561
|
+
end
|
562
|
+
|
563
|
+
def generate_foreign_key fk, indent
|
564
|
+
# '-- '+fk.inspect
|
565
|
+
" " * indent + "FOREIGN KEY (" +
|
566
|
+
fk.all_foreign_key_field.map{|fkf| safe_column_name fkf.component}*", " +
|
567
|
+
") REFERENCES <a href=\"#LDMD_#{table_name fk.composite}\">#{table_name fk.composite}</a> (" +
|
568
|
+
fk.all_index_field.map{|ixf| safe_column_name ixf.component}*", " +
|
569
|
+
")"
|
570
|
+
end
|
571
|
+
|
572
|
+
def reserved_words
|
573
|
+
@reserved_words ||= %w{ }
|
574
|
+
end
|
575
|
+
|
576
|
+
def is_reserved_word w
|
577
|
+
@reserved_word_hash ||=
|
578
|
+
reserved_words.inject({}) do |h,w|
|
579
|
+
h[w] = true
|
580
|
+
h
|
581
|
+
end
|
582
|
+
@reserved_word_hash[w.upcase]
|
583
|
+
end
|
584
|
+
|
585
|
+
def go s = ''
|
586
|
+
"#{s}\nGO\n" # REVISIT: This is an SQL-Serverism. Move it to a subclass.
|
587
|
+
end
|
588
|
+
|
589
|
+
def escape s, max = table_name_max
|
590
|
+
# Escape SQL keywords and non-identifiers
|
591
|
+
if s.size > max
|
592
|
+
excess = s[max..-1]
|
593
|
+
s = s[0...max-(excess.size/8)] +
|
594
|
+
Digest::SHA1.hexdigest(excess)[0...excess.size/8]
|
595
|
+
end
|
596
|
+
|
597
|
+
if s =~ /[^A-Za-z0-9_]/ || is_reserved_word(s)
|
598
|
+
"[#{s}]"
|
599
|
+
else
|
600
|
+
s
|
601
|
+
end
|
602
|
+
end
|
603
|
+
|
604
|
+
# Return SQL type and (modified?) length for the passed base type
|
605
|
+
def normalise_type(type, length)
|
606
|
+
sql_type = case type
|
607
|
+
when /^Auto ?Counter$/
|
608
|
+
'int'
|
609
|
+
|
610
|
+
when /^Unsigned ?Integer$/,
|
611
|
+
/^Signed ?Integer$/,
|
612
|
+
/^Unsigned ?Small ?Integer$/,
|
613
|
+
/^Signed ?Small ?Integer$/,
|
614
|
+
/^Unsigned ?Tiny ?Integer$/
|
615
|
+
s = case
|
616
|
+
when length == nil
|
617
|
+
'int'
|
618
|
+
when length <= 8
|
619
|
+
'tinyint'
|
620
|
+
when length <= 16
|
621
|
+
'smallint'
|
622
|
+
when length <= 32
|
623
|
+
'int'
|
624
|
+
else
|
625
|
+
'bigint'
|
626
|
+
end
|
627
|
+
length = nil
|
628
|
+
s
|
629
|
+
|
630
|
+
when /^Decimal$/
|
631
|
+
'decimal'
|
632
|
+
|
633
|
+
when /^Fixed ?Length ?Text$/, /^Char$/
|
634
|
+
'char'
|
635
|
+
when /^Variable ?Length ?Text$/, /^String$/
|
636
|
+
'varchar'
|
637
|
+
when /^Large ?Length ?Text$/, /^Text$/
|
638
|
+
'text'
|
639
|
+
|
640
|
+
when /^Date ?And ?Time$/, /^Date ?Time$/
|
641
|
+
'datetime'
|
642
|
+
when /^Date$/
|
643
|
+
'datetime' # SQLSVR 2K5: 'date'
|
644
|
+
when /^Time$/
|
645
|
+
'datetime' # SQLSVR 2K5: 'time'
|
646
|
+
when /^Auto ?Time ?Stamp$/
|
647
|
+
'timestamp'
|
648
|
+
|
649
|
+
when /^Guid$/
|
650
|
+
'uniqueidentifier'
|
651
|
+
when /^Money$/
|
652
|
+
'decimal'
|
653
|
+
when /^Picture ?Raw ?Data$/, /^Image$/
|
654
|
+
'image'
|
655
|
+
when /^Variable ?Length ?Raw ?Data$/, /^Blob$/
|
656
|
+
'varbinary'
|
657
|
+
when /^BIT$/
|
658
|
+
'bit'
|
659
|
+
when /^BOOLEAN$/
|
660
|
+
'boolean'
|
661
|
+
else type # raise "SQL type unknown for standard type #{type}"
|
662
|
+
end
|
663
|
+
[sql_type, length]
|
664
|
+
end
|
665
|
+
|
666
|
+
def sql_value(value)
|
667
|
+
value.is_literal_string ? sql_string(value.literal) : value.literal
|
668
|
+
end
|
669
|
+
|
670
|
+
def sql_string(str)
|
671
|
+
"'" + str.gsub(/'/,"''") + "'"
|
672
|
+
end
|
673
|
+
|
674
|
+
def check_clause column_name, value_constraint
|
675
|
+
" CHECK(" +
|
676
|
+
value_constraint.all_allowed_range_sorted.map do |ar|
|
677
|
+
vr = ar.value_range
|
678
|
+
min = vr.minimum_bound
|
679
|
+
max = vr.maximum_bound
|
680
|
+
if (min && max && max.value.literal == min.value.literal)
|
681
|
+
"#{column_name} = #{sql_value(min.value)}"
|
682
|
+
else
|
683
|
+
inequalities = [
|
684
|
+
min && "#{column_name} >#{min.is_inclusive ? "=" : ""} #{sql_value(min.value)}",
|
685
|
+
max && "#{column_name} <#{max.is_inclusive ? "=" : ""} #{sql_value(max.value)}"
|
686
|
+
].compact
|
687
|
+
inequalities.size > 1 ? "(" + inequalities*" AND " + ")" : inequalities[0]
|
688
|
+
end
|
689
|
+
end*" OR " +
|
690
|
+
")"
|
691
|
+
end
|
692
|
+
|
693
|
+
MM = ActiveFacts::Metamodel unless const_defined?(:MM)
|
694
|
+
end
|
695
|
+
end
|
696
|
+
publish_generator Doc::LDM
|
697
|
+
end
|
698
|
+
end
|