expressir 2.4.36-arm-linux → 2.4.38-arm-linux

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,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expressir
4
+ module P21
5
+ # ISO 10303-11 9.2.5 instance typing: which entities a Part 21
6
+ # instance conforms to. A simple record conforms to its keyword
7
+ # entity plus that entity's supertype closure; a complex record
8
+ # conforms to the union of its records' closures. ONEOF
9
+ # exclusivity (9.2.5.2) is reported per complex record.
10
+ class Typing
11
+ Result = Struct.new(:entities, :issues, keyword_init: true)
12
+
13
+ def initialize(schema, short_names: nil)
14
+ @schema = schema
15
+ @short_names = short_names&.transform_keys(&:downcase)
16
+ @results = {}.compare_by_identity
17
+ end
18
+
19
+ def of(record)
20
+ result(record).entities
21
+ end
22
+
23
+ def issues(record)
24
+ result(record).issues
25
+ end
26
+
27
+ def result(record)
28
+ @results[record] ||= compute(record)
29
+ end
30
+
31
+ private
32
+
33
+ def compute(record)
34
+ parts = record.is_a?(ComplexRecord) ? record.records : [record]
35
+ entities = []
36
+ leaves = []
37
+ issues = []
38
+ seen_leaves = {}
39
+ parts.each do |part|
40
+ entity = Validator.find_entity(@schema, part.keyword, @short_names)
41
+ if entity.nil?
42
+ issues << "keyword '#{part.keyword}' is not declared in " \
43
+ "schema '#{@schema.id}'"
44
+ next
45
+ end
46
+
47
+ key = entity.id&.safe_downcase
48
+ if seen_leaves[key]
49
+ issues << "complex instance instantiates '#{entity.id}' " \
50
+ "twice (9.2.5)"
51
+ end
52
+ seen_leaves[key] = true
53
+ leaves << entity
54
+ entities.push(entity, *entity.supertype_closure)
55
+ end
56
+ issues.concat(oneof_issues(leaves))
57
+ entities.uniq! { |entity| entity.id&.safe_downcase }
58
+ Result.new(entities: entities, issues: issues)
59
+ end
60
+
61
+ def oneof_issues(leaves)
62
+ leaves.combination(2).filter_map do |left, right|
63
+ next unless shared_oneof_group?(left, right)
64
+
65
+ "complex instance combines '#{left.id}' and '#{right.id}' " \
66
+ "in one ONEOF group (9.2.5.2)"
67
+ end
68
+ end
69
+
70
+ def shared_oneof_group?(left, right)
71
+ oneof_groups(left).any? do |group|
72
+ group.include?(left.id&.safe_downcase) &&
73
+ group.include?(right.id&.safe_downcase)
74
+ end
75
+ end
76
+
77
+ # ONEOF groups constraining an entity: its own supertype
78
+ # expression, every supertype's expression, and the ONEOF
79
+ # subtype constraints applied to any of them (9.7).
80
+ def oneof_groups(entity)
81
+ @oneof_groups ||= {}
82
+ key = entity.id&.safe_downcase
83
+ @oneof_groups[key] ||= begin
84
+ groups = []
85
+ [entity, *entity.supertype_closure].each do |supertype|
86
+ expression = supertype.supertype_expression
87
+ groups << oneof_members(expression) if oneof?(expression)
88
+ subtype_constraint_groups(supertype, groups)
89
+ end
90
+ groups
91
+ end
92
+ end
93
+
94
+ def subtype_constraint_groups(supertype, groups)
95
+ Array(@schema.subtype_constraints).each do |constraint|
96
+ next unless constraint.applies_to&.id&.safe_downcase == supertype.id&.safe_downcase
97
+
98
+ expression = constraint.supertype_expression
99
+ groups << oneof_members(expression) if oneof?(expression)
100
+ end
101
+ end
102
+
103
+ def oneof?(expression)
104
+ expression.is_a?(Model::SupertypeExpressions::OneofSupertypeExpression)
105
+ end
106
+
107
+ def oneof_members(expression)
108
+ Array(expression.operands).filter_map do |operand|
109
+ next unless operand.respond_to?(:id) && operand.id
110
+
111
+ operand.id.safe_downcase
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -20,13 +20,17 @@ module Expressir
20
20
  # clause 12.3) to schema entity names, e.g. {"CPT" => "cartesian_point"}.
21
21
  def validate(population, schema, short_names: nil)
22
22
  short_names = short_names&.transform_keys(&:downcase)
23
+ typing = Typing.new(schema, short_names: short_names)
23
24
  notes = []
24
25
  check_file_schema(notes, population, schema)
26
+ check_sections(notes, population, schema)
25
27
  population.instances.each do |id, instance|
26
28
  if instance.is_a?(ComplexRecord)
27
- check_complex(notes, schema, population, id, instance, short_names)
29
+ check_complex(notes, schema, population, id, instance,
30
+ short_names, typing)
28
31
  else
29
- check_simple(notes, schema, population, id, instance, short_names)
32
+ check_simple(notes, schema, population, id, instance,
33
+ short_names, typing)
30
34
  end
31
35
  end
32
36
  notes
@@ -49,6 +53,24 @@ module Expressir
49
53
  message: message)
50
54
  end
51
55
 
56
+ # Annex E (ISO 10303-21 clause 11.2): each DATA section's schema
57
+ # contexts must intersect the declared schema list.
58
+ def check_sections(notes, population, schema)
59
+ known = ([schema.id] +
60
+ Array(population.header["FILE_SCHEMA"]).flatten)
61
+ .filter_map { |name| name.to_s.downcase unless name.to_s.empty? }
62
+ Array(population.sections).each do |section|
63
+ next if section.contexts.empty?
64
+ next if section.contexts.any? do |context|
65
+ known.include?(context.to_s.downcase)
66
+ end
67
+
68
+ note!(notes, :p21_section_context, :warning, nil,
69
+ "DATA section '#{section.name}' context(s) " \
70
+ "#{section.contexts.join(', ')} are not in the schema list")
71
+ end
72
+ end
73
+
52
74
  def check_file_schema(notes, population, schema)
53
75
  declared = Array(population.header["FILE_SCHEMA"]).flatten.first
54
76
  return unless declared && schema.id
@@ -58,7 +80,8 @@ module Expressir
58
80
  "FILE_SCHEMA '#{declared}' does not match schema '#{schema.id}'")
59
81
  end
60
82
 
61
- def check_simple(notes, schema, population, id, record, short_names)
83
+ def check_simple(notes, schema, population, id, record, short_names,
84
+ typing)
62
85
  entity = find_entity(schema, record.keyword, short_names)
63
86
  unless entity
64
87
  note!(notes, :p21_unknown_entity, :error, id,
@@ -67,10 +90,11 @@ module Expressir
67
90
  end
68
91
 
69
92
  check_params(notes, schema, population, id, record.params,
70
- mapped_slots(entity))
93
+ mapped_slots(entity), typing)
71
94
  end
72
95
 
73
- def check_complex(notes, schema, population, id, complex, short_names)
96
+ def check_complex(notes, schema, population, id, complex, short_names,
97
+ typing)
74
98
  pairs = complex.records.filter_map do |record|
75
99
  entity = find_entity(schema, record.keyword, short_names)
76
100
  unless entity
@@ -84,9 +108,14 @@ module Expressir
84
108
  return if pairs.empty?
85
109
 
86
110
  check_clique(notes, id, pairs.map { |_, entity| entity })
111
+ typing.issues(complex).each do |message|
112
+ note!(notes, :check_p21_complex_instantiation, :error, id,
113
+ "#{id}: #{message}")
114
+ end
87
115
  pairs.each do |record, entity|
88
116
  slots = explicit_attributes(entity).map { |attr| [attr, false] }
89
- check_params(notes, schema, population, id, record.params, slots)
117
+ check_params(notes, schema, population, id, record.params, slots,
118
+ typing)
90
119
  end
91
120
  end
92
121
 
@@ -120,14 +149,14 @@ module Expressir
120
149
  end + explicit_attributes(entity).map { |attr| [attr, false] }
121
150
  end
122
151
 
123
- def check_params(notes, schema, population, id, params, slots)
152
+ def check_params(notes, schema, population, id, params, slots, typing)
124
153
  if params.size != slots.size
125
154
  note!(notes, :p21_arity_mismatch, :error, id,
126
155
  "#{id}: #{params.size} values for #{slots.size} mapped attributes")
127
156
  end
128
157
 
129
158
  slots.zip(params).each do |(attr, derived_slot), value|
130
- next if value.nil?
159
+ next if value.nil? || value == :unset
131
160
 
132
161
  if derived_slot
133
162
  next if value == :derived
@@ -138,6 +167,11 @@ module Expressir
138
167
  end
139
168
 
140
169
  check_value(notes, schema, population, id, attr, value)
170
+ slot_type_issues(schema, population, attr&.type, value, typing)
171
+ .each do |(category, message)|
172
+ note!(notes, FINDING_IDS.fetch(category), :error, id,
173
+ "#{id}: #{message}")
174
+ end
141
175
  end
142
176
  end
143
177
 
@@ -147,6 +181,9 @@ module Expressir
147
181
  return unless value.match?(/\A#\d+\z/)
148
182
 
149
183
  return if population.instances.key?(value)
184
+ # An id bound in the REFERENCE section names an external
185
+ # instance (ISO 10303-21 clause 10); resolution is remote.
186
+ return if population.references.key?(value)
150
187
 
151
188
  note!(notes, :p21_unknown_reference, :error, id,
152
189
  "#{id}: reference #{value} has no instance")
@@ -208,6 +245,380 @@ module Expressir
208
245
  underlying = declaration&.underlying_type
209
246
  underlying.is_a?(Model::DataTypes::Enumeration) ? Array(underlying.items) : nil
210
247
  end
248
+
249
+ # ---- TODO.instance/01: value/type conformance ----
250
+ # ISO 10303-11 ch.8 domains over the clause-12 slots. Every
251
+ # finding carries its clause: simple types 8.1.x, aggregation
252
+ # types 8.2.x, named types 8.4.x, attribute domains 9.2.1,
253
+ # complex instantiation 9.2.5/9.2.5.2, domain rules 9.2.2.2.
254
+
255
+ FINDING_IDS = {
256
+ domain: :check_p21_value_domain,
257
+ aggregate: :check_p21_aggregate,
258
+ reference: :check_p21_reference_type,
259
+ select: :check_p21_select_arm,
260
+ complex: :check_p21_complex_instantiation,
261
+ }.freeze
262
+ MAX_TYPE_DEPTH = 16
263
+ PRIMITIVE_DOMAINS = {
264
+ Model::DataTypes::Integer => ["INTEGER", "an unquoted integer (8.1.3)"],
265
+ Model::DataTypes::Real => ["REAL", "a number (8.1.2)"],
266
+ Model::DataTypes::Number => ["NUMBER", "a number (8.1.1)"],
267
+ Model::DataTypes::String => ["STRING", "a single-quoted string (8.1.6)"],
268
+ Model::DataTypes::Binary => ["BINARY", "a binary literal (8.1.7)"],
269
+ Model::DataTypes::Boolean => ["BOOLEAN", ".T. or .F. (8.1.5)"],
270
+ Model::DataTypes::Logical => ["LOGICAL", ".T., .F. or .U. (8.1.4)"],
271
+ }.freeze
272
+ private_constant :FINDING_IDS, :MAX_TYPE_DEPTH, :PRIMITIVE_DOMAINS
273
+
274
+ def slot_type_issues(schema, population, type, value, typing, depth = 0,
275
+ seen = {}.compare_by_identity)
276
+ return [] unless type.is_a?(Model::ModelElement)
277
+ return [] if depth >= MAX_TYPE_DEPTH
278
+ if value.is_a?(TypedValue)
279
+ return typed_value_issues(schema, population, type, value, typing,
280
+ depth, seen)
281
+ end
282
+
283
+ case type
284
+ when Model::Declarations::Type
285
+ defined_type_issues(schema, population, type, value, typing,
286
+ depth, seen)
287
+ when Model::Declarations::Entity
288
+ reference_issues(schema, population, value, type, typing)
289
+ when Model::References::SimpleReference
290
+ reference_slot_issues(schema, population, type, value, typing,
291
+ depth, seen)
292
+ when Model::DataTypes::List, Model::DataTypes::Array,
293
+ Model::DataTypes::Set, Model::DataTypes::Bag
294
+ aggregate_issues(schema, population, type, value, typing, depth, seen)
295
+ when Model::DataTypes::Select
296
+ select_issues(schema, population, type, value, typing, depth, seen)
297
+ when Model::DataTypes::Enumeration
298
+ [] # 8.4.1 membership is checked by check_value
299
+ else
300
+ primitive_issues(type, value)
301
+ end
302
+ end
303
+
304
+ def reference_slot_issues(schema, population, type, value, typing,
305
+ depth, seen)
306
+ declaration = find_type(schema, type.id.to_s)
307
+ if declaration.is_a?(Model::Declarations::Type)
308
+ return defined_type_issues(schema, population, declaration, value,
309
+ typing, depth, seen)
310
+ end
311
+
312
+ entity = find_entity(schema, type.id.to_s)
313
+ return [] unless entity
314
+
315
+ reference_issues(schema, population, value, entity, typing)
316
+ end
317
+
318
+ def defined_type_issues(schema, population, declaration, value, typing,
319
+ depth, seen)
320
+ return [] if seen.include?(declaration)
321
+
322
+ seen[declaration] = true
323
+ domain_rule_issues(schema, value, declaration) +
324
+ slot_type_issues(schema, population, declaration.underlying_type,
325
+ value, typing, depth + 1, seen)
326
+ end
327
+
328
+ def domain_rule_issues(schema, value, declaration)
329
+ self_value = domain_value(value, declaration)
330
+ return [] if self_value.nil?
331
+
332
+ Array(declaration.where_rules).filter_map do |rule|
333
+ next unless rule.expression
334
+
335
+ env = Eval::Environment.new(schema: schema).define("self", self_value)
336
+ result = Eval::Expression.evaluate(rule.expression, env)
337
+ next unless logical_false?(result)
338
+
339
+ [:domain,
340
+ "value violates domain rule '#{rule.id}' of " \
341
+ "TYPE '#{declaration.id}' (9.2.2.2)"]
342
+ rescue StandardError
343
+ next # an unevaluable domain rule is not a finding here
344
+ end
345
+ end
346
+
347
+ def domain_value(value, declaration)
348
+ case value
349
+ when Integer, Float then value
350
+ when String then reference?(value) ? nil : value
351
+ when Enumeration
352
+ Eval::Values::Enumeration.new(type: declaration.id, item: value.name)
353
+ when Binary then Eval::Values::Binary.new(bits: value.hex)
354
+ end
355
+ end
356
+
357
+ def logical_false?(result)
358
+ return true if [false, Eval::Values::LOGICAL_FALSE].include?(result)
359
+ return result.value == Eval::Values::LOGICAL_FALSE if result.is_a?(Eval::Values::Logical)
360
+
361
+ false
362
+ end
363
+
364
+ def primitive_issues(type, value)
365
+ domain, expectation = PRIMITIVE_DOMAINS[type.class]
366
+ return [] unless domain
367
+
368
+ ok = case type
369
+ when Model::DataTypes::Integer then value.is_a?(Integer)
370
+ when Model::DataTypes::Real, Model::DataTypes::Number
371
+ value.is_a?(Numeric)
372
+ when Model::DataTypes::String
373
+ value.is_a?(String) && !reference?(value)
374
+ when Model::DataTypes::Binary then value.is_a?(Binary)
375
+ when Model::DataTypes::Boolean, Model::DataTypes::Logical
376
+ logical_literal?(value)
377
+ end
378
+ return [] if ok
379
+
380
+ [[:domain, "#{domain} slot requires #{expectation}"]]
381
+ end
382
+
383
+ def logical_literal?(value)
384
+ return true if [true, false].include?(value)
385
+
386
+ value.is_a?(Enumeration) && %w[T F U].include?(value.name.to_s.upcase)
387
+ end
388
+
389
+ def reference_issues(_schema, population, value, entity, typing)
390
+ return [] unless reference?(value)
391
+
392
+ target = population.instances[value]
393
+ return [] unless target
394
+
395
+ conformed = typing.of(target)
396
+ return [] if conformed.any? do |candidate|
397
+ candidate.id&.safe_downcase == entity.id&.safe_downcase
398
+ end
399
+
400
+ [[:reference,
401
+ "reference #{value} conforms to " \
402
+ "#{conformed.filter_map(&:id).join(', ')} but '#{entity.id}' is " \
403
+ "required (9.2.1)"]]
404
+ end
405
+
406
+ def typed_value_issues(schema, population, _type, value, typing, depth,
407
+ seen)
408
+ entity = find_entity(schema, value.keyword)
409
+ if entity
410
+ return reference_issues(schema, population, value.value, entity,
411
+ typing)
412
+ end
413
+
414
+ declaration = find_type(schema, value.keyword)
415
+ return [] unless declaration.is_a?(Model::Declarations::Type)
416
+
417
+ underlying = declaration.underlying_type
418
+ inner = value.value
419
+ if inner.is_a?(Array) && inner.size == 1 && !aggregate?(underlying)
420
+ inner = inner.first
421
+ end
422
+ slot_type_issues(schema, population, underlying, inner, typing,
423
+ depth + 1, seen)
424
+ end
425
+
426
+ def aggregate?(type)
427
+ type.is_a?(Model::DataTypes::List) || type.is_a?(Model::DataTypes::Array) ||
428
+ type.is_a?(Model::DataTypes::Set) || type.is_a?(Model::DataTypes::Bag)
429
+ end
430
+
431
+ def aggregate_issues(schema, population, type, value, typing, depth,
432
+ seen)
433
+ unless value.is_a?(Array)
434
+ return [[:domain, "#{aggregate_name(type)} attribute requires an " + "aggregate value (#{aggregate_section(type)})"]]
435
+ end
436
+
437
+ aggregate_bound_issues(type, value) +
438
+ aggregate_unique_issues(type, value) +
439
+ aggregate_element_issues(schema, population, type, value, typing,
440
+ depth, seen)
441
+ end
442
+
443
+ def aggregate_bound_issues(type, value)
444
+ lower, upper = aggregate_bounds(type)
445
+ issues = []
446
+ if lower && value.size < lower
447
+ issues << [:aggregate, "#{aggregate_name(type)} has #{value.size} " + "element(s), below the bound #{lower} " + "(#{aggregate_section(type)})"]
448
+ end
449
+ if upper && value.size > upper
450
+ issues << [:aggregate, "#{aggregate_name(type)} has #{value.size} " + "element(s), above the bound #{upper} " + "(#{aggregate_section(type)})"]
451
+ end
452
+ issues
453
+ end
454
+
455
+ def aggregate_bounds(type)
456
+ [bound_value(type.bound1), bound_value(type.bound2)]
457
+ end
458
+
459
+ def bound_value(bound)
460
+ return nil unless bound.is_a?(Model::Literals::Integer)
461
+
462
+ bound.value.to_i
463
+ end
464
+
465
+ def aggregate_unique_issues(type, value)
466
+ return [] unless type.is_a?(Model::DataTypes::Set)
467
+
468
+ keys = value.map { |element| p21_value_key(element) }
469
+ return [] if keys.uniq.size == keys.size
470
+
471
+ [[:aggregate, "SET has duplicate elements (8.2.4)"]]
472
+ end
473
+
474
+ def aggregate_element_issues(schema, population, type, value, typing,
475
+ depth, _seen)
476
+ issues = []
477
+ value.each_with_index do |element, index|
478
+ unset = element.nil? || element == :unset
479
+ if unset
480
+ next unless type.is_a?(Model::DataTypes::Array) && !type.optional
481
+
482
+ issues << [:aggregate, "ARRAY element #{index} is unset but the array is not OPTIONAL (8.2.1)"]
483
+ next
484
+ end
485
+
486
+ # Sibling elements re-walk the same types with different
487
+ # values: only the walk along one nesting path shares `seen`.
488
+ slot_type_issues(schema, population, type.base_type, element,
489
+ typing, depth + 1, {}.compare_by_identity)
490
+ .each do |(category, message)|
491
+ issues << [category, "element #{index}: #{message}"]
492
+ end
493
+ end
494
+ issues
495
+ end
496
+
497
+ def select_issues(schema, population, type, value, typing, depth, seen)
498
+ arms = select_arms(schema, type, seen)
499
+ return [] if arms.empty?
500
+
501
+ select_arm_issues(schema, population, value, arms, typing, depth, seen)
502
+ end
503
+
504
+ # Leaf domains of a SELECT: item entities, item defined types, and
505
+ # nested selects, plus the BASED_ON chain (8.4.2).
506
+ def select_arms(schema, type, seen)
507
+ return [] if seen.include?(type)
508
+
509
+ seen[type] = true
510
+ arms = []
511
+ Array(type.items).each do |item|
512
+ next unless item.respond_to?(:id) && item.id
513
+
514
+ declaration = find_type(schema, item.id.to_s)
515
+ if declaration.is_a?(Model::Declarations::Type)
516
+ underlying = declaration.underlying_type
517
+ if underlying.is_a?(Model::DataTypes::Select)
518
+ arms.concat(select_arms(schema, underlying, seen))
519
+ else
520
+ arms << [:type, declaration]
521
+ end
522
+ next
523
+ end
524
+
525
+ entity = find_entity(schema, item.id.to_s)
526
+ arms << [:entity, entity] if entity
527
+ end
528
+ base = based_on_select(schema, type)
529
+ arms.concat(select_arms(schema, base, seen)) if base
530
+ arms
531
+ end
532
+
533
+ def based_on_select(schema, type)
534
+ based = type.based_on
535
+ return nil unless based.respond_to?(:id) && based.id
536
+
537
+ declaration = find_type(schema, based.id.to_s)
538
+ return nil unless declaration.is_a?(Model::Declarations::Type)
539
+
540
+ underlying = declaration.underlying_type
541
+ underlying.is_a?(Model::DataTypes::Select) ? underlying : nil
542
+ end
543
+
544
+ def select_arm_issues(schema, population, value, arms, typing, depth,
545
+ seen)
546
+ if reference?(value)
547
+ target = population.instances[value]
548
+ return [] unless target
549
+
550
+ conformed = typing.of(target)
551
+ ok = arms.any? do |(kind, node)|
552
+ kind == :entity &&
553
+ conformed.any? do |candidate|
554
+ candidate.id&.safe_downcase == node.id&.safe_downcase
555
+ end
556
+ end
557
+ return [] if ok
558
+
559
+ return [[:select, "reference #{value} fits no arm of the SELECT (8.4.2)"]]
560
+ end
561
+
562
+ if value.is_a?(TypedValue)
563
+ ok = arms.any? do |(_kind, node)|
564
+ node.id&.safe_downcase == value.keyword&.downcase
565
+ end
566
+ return [] if ok
567
+
568
+ return [[:select, "typed value '#{value.keyword}' fits no arm of the SELECT (8.4.2)"]]
569
+ end
570
+
571
+ return [] if value.is_a?(Enumeration) # 8.4.1 membership via check_value
572
+
573
+ literal_ok = arms.any? do |(kind, node)|
574
+ kind == :type && slot_type_issues(
575
+ schema, population, node.underlying_type, value, typing,
576
+ depth + 1, seen
577
+ ).empty?
578
+ end
579
+ return [] if literal_ok
580
+
581
+ [[:select, "value #{p21_display(value)} fits no arm of the SELECT (8.4.2)"]]
582
+ end
583
+
584
+ def reference?(value)
585
+ value.is_a?(String) && value.match?(/\A#\d+\z/)
586
+ end
587
+
588
+ def p21_value_key(value)
589
+ case value
590
+ when Enumeration then [:enum, value.name&.downcase]
591
+ when Binary then [:binary, value.hex]
592
+ when Array then [:list, *value.map { |element| p21_value_key(element) }]
593
+ else [:value, value]
594
+ end
595
+ end
596
+
597
+ def p21_display(value)
598
+ case value
599
+ when Enumeration then ".#{value.name}."
600
+ when Binary then "%#{value.hex}"
601
+ else value.to_s
602
+ end
603
+ end
604
+
605
+ def aggregate_name(type)
606
+ case type
607
+ when Model::DataTypes::List then "LIST"
608
+ when Model::DataTypes::Array then "ARRAY"
609
+ when Model::DataTypes::Set then "SET"
610
+ else "BAG"
611
+ end
612
+ end
613
+
614
+ def aggregate_section(type)
615
+ case type
616
+ when Model::DataTypes::List then "8.2.2"
617
+ when Model::DataTypes::Array then "8.2.1"
618
+ when Model::DataTypes::Set then "8.2.4"
619
+ else "8.2.3"
620
+ end
621
+ end
211
622
  end
212
623
  end
213
624
  end