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