activefacts-compositions 1.9.6 → 1.9.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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
|