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.
@@ -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