Module: OpenEHR::AQL::PredicateEvaluator

Defined in:
lib/openehr/aql/engine/predicate_evaluator.rb

Overview

Evaluates a WHERE clause's boolean expression tree against a Binding and the query's runtime params. E5 scope: comparisons (reusing PathEvaluator for both sides), AND/OR/NOT and EXISTS. E12 adds LIKE (glob match, not SQL's %/_); E13 adds MATCHES against a literal value list (a URI/TERMINOLOGY(...) operand names an external value-set lookup this engine has no terminology service wired for, so those raise a clear ExecutionError rather than silently matching everything). Generic functionCall operands are added by a later engine milestone.

Constant Summary collapse

COMPARATORS =
{
  '=' => :==, '!=' => :!=, '<' => :<, '<=' => :<=, '>' => :>, '>=' => :>=
}.freeze
GLOB_TO_REGEXP =

LIKE's glob syntax (AQL spec, not SQL's %/_): '?' matches exactly one character, '*' matches zero or more, anything else is a literal character - and the whole value must match, not a substring.

{ '*' => '.*', '?' => '.' }.freeze

Class Method Summary collapse

Class Method Details

.compare(left, operator, right) ⇒ Object



126
127
128
129
# File 'lib/openehr/aql/engine/predicate_evaluator.rb', line 126

def compare(left, operator, right)
  method = COMPARATORS.fetch(operator) { raise ExecutionError, "unknown comparison operator #{operator.inspect}" }
  left.public_send(method, right)
end

.evaluate_comparison(comparison, binding, params) ⇒ Object

A comparison against an absent (nil) value is neither true nor false in AQL/SQL terms - it simply fails to select the row, the same as SQL's NULL-comparison-is-UNKNOWN convention.



105
106
107
108
109
110
111
# File 'lib/openehr/aql/engine/predicate_evaluator.rb', line 105

def evaluate_comparison(comparison, binding, params)
  left = resolve_operand(comparison.left, binding, params)
  right = resolve_operand(comparison.right, binding, params)
  return false if left.nil? || right.nil?

  compare(left, comparison.operator, right)
end

.evaluate_like(like_expr, binding, params) ⇒ Object



49
50
51
52
53
54
55
# File 'lib/openehr/aql/engine/predicate_evaluator.rb', line 49

def evaluate_like(like_expr, binding, params)
  value = PathEvaluator.evaluate(like_expr.path, binding)
  pattern = resolve_operand(like_expr.operand, binding, params)
  return false if value.nil? || pattern.nil?

  like_regexp(pattern).match?(value.to_s)
end

.evaluate_matches(matches_expr, binding, params) ⇒ Object



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/openehr/aql/engine/predicate_evaluator.rb', line 62

def evaluate_matches(matches_expr, binding, params)
  value = PathEvaluator.evaluate(matches_expr.path, binding)
  return false if value.nil?

  case matches_expr.operand
  when Model::MatchesValueList
    matches_value_list?(value, matches_expr.operand, params)
  when Model::UriRef, Model::TerminologyFunctionCall
    raise ExecutionError,
          "MATCHES against a #{matches_expr.operand.class} names an external terminology service lookup " \
          '(a value-set expansion), which this engine has none wired in for - ' \
          'OpenEHR::TerminologyService only validates a single known code, it cannot expand a value set'
  else
    raise ExecutionError, "cannot evaluate a #{matches_expr.operand.class} MATCHES operand yet"
  end
end

.like_regexp(pattern) ⇒ Object



57
58
59
60
# File 'lib/openehr/aql/engine/predicate_evaluator.rb', line 57

def like_regexp(pattern)
  body = pattern.chars.map { |char| GLOB_TO_REGEXP[char] || Regexp.escape(char) }.join
  Regexp.new("\\A#{body}\\z", Regexp::MULTILINE)
end

.lookup_param(parameter, params) ⇒ Object



119
120
121
122
123
124
# File 'lib/openehr/aql/engine/predicate_evaluator.rb', line 119

def lookup_param(parameter, params)
  return params[parameter.name.to_sym] if params.key?(parameter.name.to_sym)
  return params[parameter.name] if params.key?(parameter.name)

  raise UnboundParameterError, "unbound parameter: $#{parameter.name}"
end

.matches?(expression, binding, params) ⇒ Boolean

expression is nil when there is no WHERE clause at all.

Returns:

  • (Boolean)


26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/openehr/aql/engine/predicate_evaluator.rb', line 26

def matches?(expression, binding, params)
  case expression
  when nil
    true
  when Model::Comparison
    evaluate_comparison(expression, binding, params)
  when Model::AndExpr
    matches?(expression.left, binding, params) && matches?(expression.right, binding, params)
  when Model::OrExpr
    matches?(expression.left, binding, params) || matches?(expression.right, binding, params)
  when Model::NotExpr
    !matches?(expression.operand, binding, params)
  when Model::ExistsExpr
    !PathEvaluator.evaluate(expression.path, binding).nil?
  when Model::LikeExpr
    evaluate_like(expression, binding, params)
  when Model::MatchesExpr
    evaluate_matches(expression, binding, params)
  else
    raise ExecutionError, "cannot evaluate a #{expression.class} WHERE expression yet"
  end
end

.matches_list_item_value(item, params) ⇒ Object



86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/openehr/aql/engine/predicate_evaluator.rb', line 86

def matches_list_item_value(item, params)
  case item
  when Model::Parameter
    lookup_param(item, params)
  when Model::Literal
    item.value
  when Model::TerminologyFunctionCall
    raise ExecutionError,
          "MATCHES against a #{item.class} names an external terminology service lookup " \
          '(a value-set expansion), which this engine has none wired in for - ' \
          'OpenEHR::TerminologyService only validates a single known code, it cannot expand a value set'
  else
    raise ExecutionError, "cannot evaluate a #{item.class} MATCHES value-list item yet"
  end
end

.matches_value_list?(value, value_list, params) ⇒ Boolean

Returns:

  • (Boolean)


79
80
81
82
83
84
# File 'lib/openehr/aql/engine/predicate_evaluator.rb', line 79

def matches_value_list?(value, value_list, params)
  value_list.items.any? do |item|
    candidate = matches_list_item_value(item, params)
    !candidate.nil? && value == candidate
  end
end

.resolve_operand(node, binding, params) ⇒ Object



113
114
115
116
117
# File 'lib/openehr/aql/engine/predicate_evaluator.rb', line 113

def resolve_operand(node, binding, params)
  return lookup_param(node, params) if node.is_a?(Model::Parameter)

  PathEvaluator.evaluate(node, binding)
end