activefacts 0.7.3 → 0.8.5

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.
Files changed (94) hide show
  1. data/LICENSE +19 -0
  2. data/Manifest.txt +24 -2
  3. data/Rakefile +25 -3
  4. data/bin/afgen +1 -1
  5. data/bin/cql +13 -2
  6. data/css/offline.css +3 -0
  7. data/css/orm2.css +24 -0
  8. data/css/print.css +8 -0
  9. data/css/style-print.css +357 -0
  10. data/css/style.css +387 -0
  11. data/download.html +85 -0
  12. data/examples/CQL/Address.cql +3 -3
  13. data/examples/CQL/Blog.cql +13 -14
  14. data/examples/CQL/CompanyDirectorEmployee.cql +4 -4
  15. data/examples/CQL/Death.cql +3 -2
  16. data/examples/CQL/Genealogy.cql +13 -11
  17. data/examples/CQL/Marriage.cql +2 -2
  18. data/examples/CQL/Metamodel.cql +136 -93
  19. data/examples/CQL/MultiInheritance.cql +2 -2
  20. data/examples/CQL/OilSupply.cql +14 -10
  21. data/examples/CQL/Orienteering.cql +22 -19
  22. data/examples/CQL/PersonPlaysGame.cql +3 -2
  23. data/examples/CQL/SchoolActivities.cql +4 -2
  24. data/examples/CQL/SimplestUnary.cql +1 -1
  25. data/examples/CQL/SubtypePI.cql +6 -7
  26. data/examples/CQL/Warehousing.cql +16 -19
  27. data/examples/CQL/unit.cql +584 -0
  28. data/examples/index.html +276 -0
  29. data/examples/intro.html +497 -0
  30. data/examples/local.css +20 -0
  31. data/index.html +96 -0
  32. data/lib/activefacts/api/concept.rb +48 -46
  33. data/lib/activefacts/api/constellation.rb +43 -23
  34. data/lib/activefacts/api/entity.rb +2 -2
  35. data/lib/activefacts/api/instance.rb +6 -2
  36. data/lib/activefacts/api/instance_index.rb +5 -0
  37. data/lib/activefacts/api/value.rb +8 -2
  38. data/lib/activefacts/api/vocabulary.rb +15 -10
  39. data/lib/activefacts/cql/CQLParser.treetop +109 -88
  40. data/lib/activefacts/cql/Concepts.treetop +32 -10
  41. data/lib/activefacts/cql/Context.treetop +34 -0
  42. data/lib/activefacts/cql/Expressions.treetop +9 -9
  43. data/lib/activefacts/cql/FactTypes.treetop +30 -31
  44. data/lib/activefacts/cql/Language/English.treetop +50 -0
  45. data/lib/activefacts/cql/LexicalRules.treetop +2 -1
  46. data/lib/activefacts/cql/Terms.treetop +117 -0
  47. data/lib/activefacts/cql/ValueTypes.treetop +152 -0
  48. data/lib/activefacts/cql/compiler.rb +1718 -0
  49. data/lib/activefacts/cql/parser.rb +124 -57
  50. data/lib/activefacts/generate/absorption.rb +1 -1
  51. data/lib/activefacts/generate/cql.rb +111 -100
  52. data/lib/activefacts/generate/cql/html.rb +5 -5
  53. data/lib/activefacts/generate/oo.rb +3 -3
  54. data/lib/activefacts/generate/ordered.rb +51 -19
  55. data/lib/activefacts/generate/ruby.rb +10 -8
  56. data/lib/activefacts/generate/sql/mysql.rb +14 -10
  57. data/lib/activefacts/generate/sql/server.rb +29 -24
  58. data/lib/activefacts/input/cql.rb +9 -1264
  59. data/lib/activefacts/input/orm.rb +213 -200
  60. data/lib/activefacts/persistence/columns.rb +11 -10
  61. data/lib/activefacts/persistence/index.rb +15 -18
  62. data/lib/activefacts/persistence/reference.rb +17 -17
  63. data/lib/activefacts/persistence/tables.rb +50 -51
  64. data/lib/activefacts/version.rb +1 -1
  65. data/lib/activefacts/vocabulary/extensions.rb +79 -8
  66. data/lib/activefacts/vocabulary/metamodel.rb +183 -114
  67. data/spec/absorption_ruby_spec.rb +99 -0
  68. data/spec/absorption_spec.rb +3 -4
  69. data/spec/api/constellation.rb +1 -1
  70. data/spec/api/entity_type.rb +3 -1
  71. data/spec/api/instance.rb +4 -2
  72. data/spec/api/roles.rb +8 -6
  73. data/spec/api_spec.rb +1 -2
  74. data/spec/cql/context_spec.rb +71 -0
  75. data/spec/cql/samples_spec.rb +154 -0
  76. data/spec/cql/unit_spec.rb +375 -0
  77. data/spec/cql_cql_spec.rb +31 -21
  78. data/spec/cql_mysql_spec.rb +70 -0
  79. data/spec/cql_parse_spec.rb +15 -9
  80. data/spec/cql_ruby_spec.rb +27 -13
  81. data/spec/cql_sql_spec.rb +42 -16
  82. data/spec/cql_symbol_tables_spec.rb +2 -3
  83. data/spec/cqldump_spec.rb +7 -7
  84. data/spec/helpers/file_matcher.rb +39 -0
  85. data/spec/norma_cql_spec.rb +20 -12
  86. data/spec/norma_ruby_spec.rb +6 -3
  87. data/spec/norma_sql_spec.rb +6 -3
  88. data/spec/norma_tables_spec.rb +6 -4
  89. data/spec/spec_helper.rb +27 -8
  90. data/status.html +69 -0
  91. data/why.html +60 -0
  92. metadata +34 -11
  93. data/lib/activefacts/cql/DataTypes.treetop +0 -81
  94. data/spec/cql_unit_spec.rb +0 -330
@@ -62,8 +62,8 @@ module ActiveFacts
62
62
  def value_type_dump(o)
63
63
  return unless o.supertype # An imported type
64
64
  if o.name == o.supertype.name
65
- # In ActiveFacts, parameterising a ValueType will create a new datatype
66
- # throw Can't handle parameterized value type of same name as its datatype" if ...
65
+ # In ActiveFacts, parameterising a ValueType will create a new ValueType
66
+ # throw Can't handle parameterized value type of same name as its ValueType" if ...
67
67
  end
68
68
 
69
69
  parameters =
@@ -139,13 +139,13 @@ module ActiveFacts
139
139
  # Detect standard reference-mode readings:
140
140
  forward_reading = reverse_reading = nil
141
141
  ft.all_reading.each do |reading|
142
- if reading.reading_text =~ /^\{(\d)\} has \{\d\}$/
142
+ if reading.text =~ /^\{(\d)\} has \{\d\}$/
143
143
  if reading.role_sequence.all_role_ref[$1.to_i].role == entity_role
144
144
  forward_reading = reading
145
145
  else
146
146
  reverse_reading = reading
147
147
  end
148
- elsif reading.reading_text =~ /^\{(\d)\} is of \{\d\}$/
148
+ elsif reading.text =~ /^\{(\d)\} is of \{\d\}$/
149
149
  if reading.role_sequence.all_role_ref[$1.to_i].role == value_role
150
150
  reverse_reading = reading
151
151
  else
@@ -356,7 +356,7 @@ module ActiveFacts
356
356
  }
357
357
  frequency_constraints = [] unless frequency_constraints.detect{|fc| fc[0] =~ /some/ }
358
358
 
359
- #$stderr.puts "fact_type roles (#{fact_type.all_role.map{|r| r.concept.name}*","}) default_reading '#{fact_type.preferred_reading.reading_text}' roles (#{fact_type.preferred_reading.role_sequence.all_role_ref.map{|rr| rr.role.concept.name}*","}) #{frequency_constraints.inspect}"
359
+ #$stderr.puts "fact_type roles (#{fact_type.all_role.map{|r| r.concept.name}*","}) default_reading '#{fact_type.preferred_reading.text}' roles (#{fact_type.preferred_reading.role_sequence.all_role_ref.map{|rr| rr.role.concept.name}*","}) #{frequency_constraints.inspect}"
360
360
 
361
361
  # REVISIT: Make sure that we refer to the constrained players by their common supertype
362
362
 
@@ -98,7 +98,7 @@ module ActiveFacts
98
98
  role_name = role_method
99
99
  role_name = nil if role_name == role.concept.name.snakecase
100
100
 
101
- binary_dump(role, other_role_name, other_player, one_to_one, nil, role_name, other_role_method)
101
+ binary_dump(role, other_role_name, other_player, role.is_mandatory, one_to_one, nil, role_name, other_role_method)
102
102
  end
103
103
 
104
104
  def preferred_role_name(role, is_for = nil)
@@ -117,7 +117,7 @@ module ActiveFacts
117
117
  # Unaries are a hack, with only one role for what is effectively a binary:
118
118
  if (role.fact_type.all_role.size == 1)
119
119
  return (role.role_name && role.role_name.snakecase) ||
120
- reading.reading_text.gsub(/ *\{0\} */,'').gsub(' ','_').downcase
120
+ reading.text.gsub(/ *\{0\} */,'').gsub(' ','_').downcase
121
121
  end
122
122
 
123
123
  # debug "\tleading_adjective=#{(p=preferred_role_ref).leading_adjective}, role_name=#{role.role_name}, role player=#{role.concept.name}, trailing_adjective=#{p.trailing_adjective}"
@@ -161,7 +161,7 @@ module ActiveFacts
161
161
  other_role_method = (one_to_one ? "" : "all_") +
162
162
  fact_type.entity_type.name.snakecase +
163
163
  as
164
- binary_dump(role, role_name, role.concept, one_to_one, nil, nil, other_role_method)
164
+ binary_dump(role, role_name, role.concept, true, one_to_one, nil, nil, other_role_method)
165
165
  }
166
166
  end
167
167
 
@@ -65,7 +65,7 @@ module ActiveFacts
65
65
  def value_types_dump
66
66
  done_banner = false
67
67
  @value_type_dumped = {}
68
- @vocabulary.all_feature.sort_by{|o| o.name}.each{|o|
68
+ @vocabulary.all_concept.sort_by{|o| o.name}.each{|o|
69
69
  next unless o.is_a?(ActiveFacts::Metamodel::ValueType)
70
70
 
71
71
  value_type_banner unless done_banner
@@ -90,11 +90,11 @@ module ActiveFacts
90
90
  # if possible (it's not always, there may be loops!)
91
91
  def entity_types_dump
92
92
  # Build hash tables of precursors and followers to use:
93
- precursors, followers = *build_entity_dependencies
93
+ @precursors, @followers = *build_entity_dependencies
94
94
 
95
95
  done_banner = false
96
- sorted = @vocabulary.all_feature.select{|o|
97
- o.is_a?(ActiveFacts::Metamodel::EntityType) and !o.fact_type
96
+ sorted = @vocabulary.all_concept.select{|o|
97
+ o.is_a?(ActiveFacts::Metamodel::EntityType) # and !o.fact_type
98
98
  }.sort_by{|o| o.name}
99
99
  panic = nil
100
100
  while true do
@@ -105,7 +105,7 @@ module ActiveFacts
105
105
 
106
106
  # Can we do this yet?
107
107
  if (o != panic and # We don't *have* to do it (panic mode)
108
- (p = precursors[o]) and # There might be...
108
+ (p = @precursors[o]) and # There might be...
109
109
  p.size > 0) # precursors - still blocked
110
110
  skipped_this_pass += 1
111
111
  next
@@ -115,14 +115,19 @@ module ActiveFacts
115
115
  done_banner = true
116
116
 
117
117
  # We're going to emit o - remove it from precursors of others:
118
- (followers[o]||[]).each{|f|
119
- precursors[f] -= [o]
118
+ (@followers[o]||[]).each{|f|
119
+ @precursors[f] -= [o]
120
120
  }
121
121
  count_this_pass += 1
122
122
  panic = nil
123
123
 
124
- entity_type_dump(o)
125
- released_fact_types_dump(o)
124
+ if (o.fact_type)
125
+ fact_type_dump_with_dependents(o.fact_type)
126
+ released_fact_types_dump(o)
127
+ else
128
+ entity_type_dump(o)
129
+ released_fact_types_dump(o)
130
+ end
126
131
 
127
132
  entity_type_group_end
128
133
  }
@@ -136,7 +141,7 @@ module ActiveFacts
136
141
  ":\n\t" + bad.map{|o|
137
142
  o.name +
138
143
  ": " +
139
- precursors[o].map{|p| p.name}.uniq.inspect
144
+ @precursors[o].map{|p| p.name}.uniq.inspect
140
145
  } * "\n\t" + "\n"
141
146
  else
142
147
  # Find the object that has the most followers and no fwd-ref'd supertypes:
@@ -144,7 +149,7 @@ module ActiveFacts
144
149
  panic = sorted.
145
150
  select{|o| !@concept_types_dumped[o] }.
146
151
  sort_by{|o|
147
- f = followers[o] || [];
152
+ f = @followers[o] || [];
148
153
  o.supertypes.detect{|s| !@concept_types_dumped[s] } ? 0 : -f.size
149
154
  }[0]
150
155
  # debug "Panic mode, selected #{panic.name} next"
@@ -179,7 +184,7 @@ module ActiveFacts
179
184
  role_refs = pi.role_sequence.all_role_ref.sort_by{|role_ref| role_ref.ordinal}
180
185
 
181
186
  # We need to get the adjectives for the roles from the identifying fact's preferred readings:
182
- identifying_facts = role_refs.map{|rr| rr.role.fact_type }.uniq
187
+ identifying_facts = ([o.fact_type]+role_refs.map{|rr| rr.role.fact_type }).compact.uniq
183
188
  preferred_readings = identifying_facts.inject({}){|reading_hash, fact_type|
184
189
  pr = fact_type.preferred_reading
185
190
  reading_hash[fact_type] = pr
@@ -211,12 +216,13 @@ module ActiveFacts
211
216
  def expanded_reading(reading, fact_constraints, define_role_names)
212
217
  # Find all role numbers in order of occurrence in this reading:
213
218
  role_refs = reading.role_sequence.all_role_ref.sort_by{|role_ref| role_ref.ordinal}
214
- role_numbers = reading.reading_text.scan(/\{(\d)\}/).flatten.map{|m| Integer(m) }
219
+ role_numbers = reading.text.scan(/\{(\d)\}/).flatten.map{|m| Integer(m) }
215
220
  roles = role_numbers.map{|m| role_refs[m].role }
216
- # debug "Considering #{reading.reading_text} having #{role_numbers.inspect}"
221
+ # debug "Considering #{reading.text} having #{role_numbers.inspect}"
217
222
 
218
223
  # Find the constraints that constrain frequency over each role we can verbalise:
219
224
  frequency_constraints = []
225
+ value_restrictions = []
220
226
  roles.each do |role|
221
227
  # Find a mandatory constraint that's *not* unique; this will need an extra reading
222
228
  role_is_first_in = reading.fact_type.all_reading.detect{|r|
@@ -225,6 +231,15 @@ module ActiveFacts
225
231
  }[0].role
226
232
  }
227
233
 
234
+ if vr = role.role_value_restriction
235
+ if @constraints_used[vr]
236
+ vr = nil
237
+ else
238
+ @constraints_used[vr] = true
239
+ vr = vr.describe
240
+ end
241
+ end
242
+ value_restrictions << vr
228
243
  if (role == roles.last) # First role of the reading?
229
244
  # REVISIT: With a ternary, doing this on other than the last role can be ambiguous,
230
245
  # in case both the 2nd and 3rd roles have frequencies. Think some more!
@@ -245,7 +260,7 @@ module ActiveFacts
245
260
  end
246
261
  end
247
262
 
248
- expanded = reading.expand(frequency_constraints, define_role_names)
263
+ expanded = reading.expand(frequency_constraints, define_role_names, value_restrictions)
249
264
 
250
265
  if (ft_rings = @ring_constraints_by_fact[reading.fact_type]) &&
251
266
  (ring = ft_rings.detect{|rc| !@constraints_used[rc]})
@@ -279,8 +294,8 @@ module ActiveFacts
279
294
  # This returns an array of two hash tables each keyed by an EntityType.
280
295
  # The values of each hash entry are the precursors and followers (respectively) of that entity.
281
296
  def build_entity_dependencies
282
- @vocabulary.all_feature.inject([{},{}]) { |a, o|
283
- if o.is_a?(ActiveFacts::Metamodel::EntityType) && !o.fact_type
297
+ @vocabulary.all_concept.inject([{},{}]) { |a, o|
298
+ if o.is_a?(ActiveFacts::Metamodel::EntityType)
284
299
  precursor = a[0]
285
300
  follower = a[1]
286
301
  blocked = false
@@ -289,12 +304,21 @@ module ActiveFacts
289
304
  pi.role_sequence.all_role_ref.each{|rr|
290
305
  role = rr.role
291
306
  player = role.concept
307
+ # REVISIT: If we decide to emit value types on demand, need to remove this:
292
308
  next unless player.is_a?(ActiveFacts::Metamodel::EntityType)
293
309
  # player is a precursor of o
294
310
  (precursor[o] ||= []) << player if (player != o)
295
311
  (follower[player] ||= []) << o if (player != o)
296
312
  }
297
313
  end
314
+ if o.fact_type
315
+ o.fact_type.all_role.each do |role|
316
+ next unless role.concept.is_a?(ActiveFacts::Metamodel::EntityType)
317
+ (precursor[o] ||= []) << role.concept
318
+ (follower[role.concept] ||= []) << o
319
+ end
320
+ end
321
+
298
322
  # Supertypes are precursors too:
299
323
  subtyping = o.all_type_inheritance_as_supertype
300
324
  next a if subtyping.size == 0
@@ -304,6 +328,12 @@ module ActiveFacts
304
328
  (precursor[s] ||= []) << o
305
329
  (follower[o] ||= []) << s
306
330
  }
331
+ # REVISIT: Need to use this to order ValueTypes after their supertypes
332
+ # else
333
+ # o.all_value_type_as_supertype.each { |s|
334
+ # (precursor[s] ||= []) << o
335
+ # (follower[o] ||= []) << s
336
+ # }
307
337
  end
308
338
  a
309
339
  }
@@ -317,7 +347,9 @@ module ActiveFacts
317
347
  roles.map(&:fact_type).uniq.select{|fact_type|
318
348
  # The fact type hasn't already been dumped but all its role players have
319
349
  !@fact_types_dumped[fact_type] &&
320
- !fact_type.all_role.detect{|r| !@concept_types_dumped[r.concept] }
350
+ !fact_type.all_role.detect{|r| !@concept_types_dumped[r.concept] } &&
351
+ !fact_type.entity_type
352
+ # !(fact_type.entity_type && (p = @precursors[fact_type.entity_type]) && p.size > 0)
321
353
  }.sort_by{|fact_type|
322
354
  fact_type_key(fact_type)
323
355
  }.each{|fact_type|
@@ -380,7 +412,7 @@ module ActiveFacts
380
412
  all_role_ref.
381
413
  sort_by{|role_ref| role_ref.ordinal}.
382
414
  map{|role_ref| [ role_ref.leading_adjective, role_ref.role.concept.name, role_ref.trailing_adjective ].compact*"-" } +
383
- [pr.reading_text]
415
+ [pr.text]
384
416
  else
385
417
  fact_type.all_role.map{|role| role.concept.name }
386
418
  end
@@ -52,8 +52,8 @@ module ActiveFacts
52
52
 
53
53
  return if !o.supertype && !is_special_supertype
54
54
  if o.supertype && o.name == o.supertype.name
55
- # In ActiveFacts, parameterising a ValueType will create a new datatype
56
- # throw Can't handle parameterized value type of same name as its datatype" if ...
55
+ # In ActiveFacts, parameterising a ValueType will create a new ValueType
56
+ # throw Can't handle parameterized value type of same name as its ValueType" if ...
57
57
  end
58
58
 
59
59
  length = (l = o.length) && l > 0 ? ":length => #{l}" : nil
@@ -76,7 +76,7 @@ module ActiveFacts
76
76
  if @sql and o.is_table
77
77
  puts " table"
78
78
  end
79
- puts " \# REVISIT: #{o.name} has restricted values\n" if o.value_restriction
79
+ puts " restrict #{o.value_restriction.all_allowed_range_sorted.map{|ar| ar.to_s}*", "}\n" if o.value_restriction
80
80
  puts " \# REVISIT: #{o.name} is in units of #{o.unit.name}\n" if o.unit
81
81
  roles_dump(o)
82
82
  puts " end\n\n"
@@ -147,26 +147,28 @@ module ActiveFacts
147
147
  puts " maybe :"+role_name
148
148
  end
149
149
 
150
- def binary_dump(role, role_name, role_player, one_to_one = nil, readings = nil, other_role_name = nil, other_method_name = nil)
150
+ def binary_dump(role, role_name, role_player, mandatory = nil, one_to_one = nil, readings = nil, other_role_name = nil, other_method_name = nil)
151
151
  # Find whether we need the name of the other role player, and whether it's defined yet:
152
152
  if role_name.camelcase(true) == role_player.name.sub(/^[a-z]/) {|i| i.upcase}
153
153
  # Don't use Class name if implied by rolename
154
154
  role_reference = nil
155
155
  else
156
- role_reference = concept_reference(role_player)
156
+ role_reference = ":class => "+concept_reference(role_player)
157
157
  end
158
- other_role_name = ":"+other_role_name if other_role_name
158
+ other_role_name = ":counterpart => :"+other_role_name if other_role_name
159
159
 
160
160
  line = " #{one_to_one ? "one_to_one" : "has_one" } " +
161
161
  [ ":"+role_name,
162
162
  role_reference,
163
+ mandatory ? ":mandatory => true" : nil,
163
164
  readings,
164
- other_role_name
165
+ other_role_name,
166
+ (vr = role.role_value_restriction) ? ":restrict => #{vr}" : nil
165
167
  ].compact*", "+" "
166
168
  line += " "*(48-line.length) if line.length < 48
167
169
  line += "\# See #{role_player.name}.#{other_method_name}" if other_method_name
168
170
  puts line
169
- puts " \# REVISIT: #{other_role_name} has restricted values\n" if role.role_value_restriction
171
+ #puts " \# REVISIT: #{other_role_name} has values restricted to #{role.role_value_restriction}\n" if role.role_value_restriction
170
172
  end
171
173
 
172
174
  def concept_reference concept
@@ -15,7 +15,7 @@ module ActiveFacts
15
15
  # afgen --sql/mysql[=options] <file>.cql
16
16
  # Options are comma or space separated:
17
17
  # * delay_fks Leave all foreign keys until the end, not just those that contain forward-references
18
- # * norma Translate datatypes from NORMA to SQL Server
18
+ # * norma Translate valuetypes from NORMA to SQL Server
19
19
  class MYSQL
20
20
  private
21
21
  include Persistence
@@ -227,24 +227,28 @@ module ActiveFacts
227
227
  end
228
228
 
229
229
  private
230
+ def sql_value(value)
231
+ value.is_a_string ? sql_string(value.literal) : value.literal
232
+ end
233
+
234
+ def sql_string(str)
235
+ "'" + str.gsub(/'/,"''") + "'"
236
+ end
237
+
230
238
  def check_clause(column_name, restrictions)
231
239
  return "" if restrictions.empty?
232
240
  # REVISIT: Merge all restrictions (later; now just use the first)
233
241
  " CHECK(" +
234
- restrictions[0].all_allowed_range.sort_by do |ar|
235
- # Put the allowed ranges into a defined order:
236
- ((min = ar.value_range.minimum_bound) && min.value) ||
237
- ((max = ar.value_range.maximum_bound) && max.value)
238
- end.map do |ar|
242
+ restrictions[0].all_allowed_range_sorted.map do |ar|
239
243
  vr = ar.value_range
240
244
  min = vr.minimum_bound
241
245
  max = vr.maximum_bound
242
- if (min && max && max.value == min.value)
243
- "#{column_name} = #{min.value}"
246
+ if (min && max && max.value.literal == min.value.literal)
247
+ "#{column_name} = #{sql_value(min.value)}"
244
248
  else
245
249
  inequalities = [
246
- min && "#{column_name} >#{min.is_inclusive ? "=" : ""} #{min.value}",
247
- max && "#{column_name} <#{max.is_inclusive ? "=" : ""} #{max.value}"
250
+ min && "#{column_name} >#{min.is_inclusive ? "=" : ""} #{sql_value(min.value)}",
251
+ max && "#{column_name} <#{max.is_inclusive ? "=" : ""} #{sql_value(max.value)}"
248
252
  ].compact
249
253
  inequalities.size > 1 ? "(" + inequalities*" AND " + ")" : inequalities[0]
250
254
  end
@@ -15,7 +15,7 @@ module ActiveFacts
15
15
  # afgen --sql/server[=options] <file>.cql
16
16
  # Options are comma or space separated:
17
17
  # * delay_fks Leave all foreign keys until the end, not just those that contain forward-references
18
- # * norma Translate datatypes from NORMA to SQL Server
18
+ # * norma Translate valuetypes from NORMA to SQL Server
19
19
  class SERVER
20
20
  private
21
21
  include Persistence
@@ -49,6 +49,7 @@ module ActiveFacts
49
49
  @vocabulary = @vocabulary.Vocabulary.values[0] if ActiveFacts::API::Constellation === @vocabulary
50
50
  @delay_fks = options.include? "delay_fks"
51
51
  @norma = options.include? "norma"
52
+ @underscore = options.include?("underscore") ? "_" : ""
52
53
  end
53
54
 
54
55
  def puts s
@@ -102,7 +103,7 @@ module ActiveFacts
102
103
  when "PictureRawData"; "image"
103
104
  when "VariableLengthRawData"; "varbinary"
104
105
  when "BIT"; "bit"
105
- else raise "SQL type unknown for NORMA type #{type}"
106
+ else type # raise "SQL type unknown for NORMA type #{type}"
106
107
  end
107
108
  [sql_type, length]
108
109
  end
@@ -116,7 +117,7 @@ module ActiveFacts
116
117
  delayed_foreign_keys = []
117
118
 
118
119
  @vocabulary.tables.each do |table|
119
- puts "CREATE TABLE #{escape table.name} ("
120
+ puts "CREATE TABLE #{escape table.name(@underscore)} ("
120
121
 
121
122
  pk = table.identifier_columns
122
123
  identity_column = pk[0] if pk.size == 1 && pk[0].is_auto_assigned
@@ -128,8 +129,8 @@ module ActiveFacts
128
129
 
129
130
  # We sort the columns here, not in the persistence layer, because it affects
130
131
  # the ordering of columns in an index :-(.
131
- columns = table.columns.sort_by { |column| column.name(nil) }.map do |column|
132
- name = escape column.name("")
132
+ columns = table.columns.sort_by { |column| column.name(@underscore) }.map do |column|
133
+ name = escape column.name(@underscore)
133
134
  padding = " "*(name.size >= ColumnNameMax ? 1 : ColumnNameMax-name.size)
134
135
  type, params, restrictions = column.type
135
136
  restrictions = [] if (fk_columns.include?(column)) # Don't enforce VT restrictions on FK columns
@@ -153,22 +154,22 @@ module ActiveFacts
153
154
  end.flatten
154
155
 
155
156
  pk_def = (pk.detect{|column| !column.is_mandatory} ? "UNIQUE(" : "PRIMARY KEY(") +
156
- pk.map{|column| escape column.name("")}*", " +
157
+ pk.map{|column| escape column.name(@underscore)}*", " +
157
158
  ")"
158
159
 
159
160
  inline_fks = []
160
161
  table.foreign_keys.each do |fk|
161
162
  fk_text = "FOREIGN KEY (" +
162
- fk.from_columns.map{|column| column.name}*", " +
163
- ") REFERENCES #{escape fk.to.name} (" +
164
- fk.to_columns.map{|column| column.name}*", " +
163
+ fk.from_columns.map{|column| column.name(@underscore)}*", " +
164
+ ") REFERENCES #{escape fk.to.name(@underscore)} (" +
165
+ fk.to_columns.map{|column| column.name(@underscore)}*", " +
165
166
  ")"
166
167
  if !@delay_fks and # We don't want to delay all Fks
167
168
  (tables_emitted[fk.to] or # The target table has been emitted
168
169
  fk.to == table && !fk.to_columns.detect{|column| !column.is_mandatory}) # The reference columns already have the required indexes
169
170
  inline_fks << fk_text
170
171
  else
171
- delayed_foreign_keys << ("ALTER TABLE #{escape fk.from.name}\n\tADD " + fk_text)
172
+ delayed_foreign_keys << ("ALTER TABLE #{escape fk.from.name(@underscore)}\n\tADD " + fk_text)
172
173
  end
173
174
  end
174
175
 
@@ -177,8 +178,8 @@ module ActiveFacts
177
178
  delayed_indices = []
178
179
  indices.each do |index|
179
180
  next if index.over == table && index.is_primary # Already did the primary keys
180
- abbreviated_column_names = index.abbreviated_column_names*""
181
- column_names = index.column_names
181
+ abbreviated_column_names = index.abbreviated_column_names(@underscore)*""
182
+ column_names = index.column_names(@underscore)
182
183
  column_name_list = column_names.map{|n| escape(n)}*", "
183
184
  if index.columns.all?{|column| column.is_mandatory}
184
185
  inline_indices << "UNIQUE(#{column_name_list})"
@@ -186,17 +187,17 @@ module ActiveFacts
186
187
  view_name = escape "#{index.view_name}_#{abbreviated_column_names}"
187
188
  delayed_indices <<
188
189
  %Q{CREATE VIEW dbo.#{view_name} (#{column_name_list}) WITH SCHEMABINDING AS
189
- \tSELECT #{column_name_list} FROM dbo.#{escape index.on.name}
190
+ \tSELECT #{column_name_list} FROM dbo.#{escape index.on.name(@underscore)}
190
191
  \tWHERE\t#{
191
192
  index.columns.
192
193
  select{|column| !column.is_mandatory }.
193
194
  map{|column|
194
- escape(column.name) + " IS NOT NULL"
195
+ escape(column.name(@underscore)) + " IS NOT NULL"
195
196
  }*"\n\t AND\t"
196
197
  }
197
198
  GO
198
199
 
199
- CREATE UNIQUE CLUSTERED INDEX #{escape index.name} ON dbo.#{view_name}(#{index.columns.map{|column| column.name}*", "})
200
+ CREATE UNIQUE CLUSTERED INDEX #{escape index.name} ON dbo.#{view_name}(#{index.columns.map{|column| column.name(@underscore)}*", "})
200
201
  }
201
202
  end
202
203
  end
@@ -216,24 +217,28 @@ CREATE UNIQUE CLUSTERED INDEX #{escape index.name} ON dbo.#{view_name}(#{index.c
216
217
  end
217
218
 
218
219
  private
220
+ def sql_value(value)
221
+ value.is_a_string ? sql_string(value.literal) : value.literal
222
+ end
223
+
224
+ def sql_string(str)
225
+ "'" + str.gsub(/'/,"''") + "'"
226
+ end
227
+
219
228
  def check_clause(column_name, restrictions)
220
229
  return "" if restrictions.empty?
221
230
  # REVISIT: Merge all restrictions (later; now just use the first)
222
231
  " CHECK(" +
223
- restrictions[0].all_allowed_range.sort_by do |ar|
224
- # Put the allowed ranges into a defined order:
225
- ((min = ar.value_range.minimum_bound) && min.value) ||
226
- ((max = ar.value_range.maximum_bound) && max.value)
227
- end.map do |ar|
232
+ restrictions[0].all_allowed_range_sorted.map do |ar|
228
233
  vr = ar.value_range
229
234
  min = vr.minimum_bound
230
235
  max = vr.maximum_bound
231
- if (min && max && max.value == min.value)
232
- "#{column_name} = #{min.value}"
236
+ if (min && max && max.value.literal == min.value.literal)
237
+ "#{column_name} = #{sql_value(min.value)}"
233
238
  else
234
239
  inequalities = [
235
- min && "#{column_name} >#{min.is_inclusive ? "=" : ""} #{min.value}",
236
- max && "#{column_name} <#{max.is_inclusive ? "=" : ""} #{max.value}"
240
+ min && "#{column_name} >#{min.is_inclusive ? "=" : ""} #{sql_value(min.value)}",
241
+ max && "#{column_name} <#{max.is_inclusive ? "=" : ""} #{sql_value(max.value)}"
237
242
  ].compact
238
243
  inequalities.size > 1 ? "(" + inequalities*" AND " + ")" : inequalities[0]
239
244
  end