conceptql 0.1.1 → 0.2.0
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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/CHANGELOG.md +53 -1
- data/Guardfile +52 -24
- data/conceptql.gemspec +5 -3
- data/doc/spec.md +2 -2
- data/lib/conceptql/behaviors/debuggable.rb +70 -0
- data/lib/conceptql/behaviors/dottable.rb +28 -14
- data/lib/conceptql/behaviors/metadatable.rb +97 -0
- data/lib/conceptql/behaviors/preppable.rb +20 -0
- data/lib/conceptql/cli.rb +31 -5
- data/lib/conceptql/converter.rb +65 -0
- data/lib/conceptql/debugger.rb +48 -0
- data/lib/conceptql/graph.rb +14 -13
- data/lib/conceptql/graph_nodifier.rb +49 -17
- data/lib/conceptql/nodifier.rb +31 -6
- data/lib/conceptql/operators/after.rb +25 -0
- data/lib/conceptql/operators/any_overlap.rb +15 -0
- data/lib/conceptql/operators/before.rb +21 -0
- data/lib/conceptql/{nodes/binary_operator_node.rb → operators/binary_operator_operator.rb} +12 -8
- data/lib/conceptql/{nodes/casting_node.rb → operators/casting_operator.rb} +8 -7
- data/lib/conceptql/{nodes → operators}/complement.rb +14 -11
- data/lib/conceptql/operators/concept.rb +70 -0
- data/lib/conceptql/{nodes → operators}/condition_type.rb +13 -5
- data/lib/conceptql/operators/contains.rb +17 -0
- data/lib/conceptql/operators/count.rb +26 -0
- data/lib/conceptql/operators/cpt.rb +25 -0
- data/lib/conceptql/{nodes → operators}/date_range.rb +11 -6
- data/lib/conceptql/operators/death.rb +23 -0
- data/lib/conceptql/operators/drug_type_concept.rb +21 -0
- data/lib/conceptql/{nodes → operators}/during.rb +9 -3
- data/lib/conceptql/operators/equal.rb +13 -0
- data/lib/conceptql/operators/except.rb +28 -0
- data/lib/conceptql/operators/filter.rb +21 -0
- data/lib/conceptql/{nodes → operators}/first.rb +7 -5
- data/lib/conceptql/{nodes → operators}/from.rb +2 -2
- data/lib/conceptql/operators/from_seer_visits.rb +23 -0
- data/lib/conceptql/{nodes → operators}/gender.rb +6 -6
- data/lib/conceptql/operators/hcpcs.rb +25 -0
- data/lib/conceptql/operators/icd10.rb +28 -0
- data/lib/conceptql/operators/icd9.rb +28 -0
- data/lib/conceptql/operators/icd9_procedure.rb +25 -0
- data/lib/conceptql/{nodes → operators}/intersect.rb +7 -3
- data/lib/conceptql/{nodes → operators}/last.rb +7 -5
- data/lib/conceptql/operators/loinc.rb +25 -0
- data/lib/conceptql/operators/medcode.rb +28 -0
- data/lib/conceptql/operators/medcode_procedure.rb +27 -0
- data/lib/conceptql/operators/ndc.rb +29 -0
- data/lib/conceptql/operators/numeric.rb +54 -0
- data/lib/conceptql/operators/observation_by_enttype.rb +29 -0
- data/lib/conceptql/operators/observation_period.rb +30 -0
- data/lib/conceptql/operators/occurrence.rb +70 -0
- data/lib/conceptql/operators/one_in_two_out.rb +62 -0
- data/lib/conceptql/{nodes/node.rb → operators/operator.rb} +84 -52
- data/lib/conceptql/operators/overlapped_by.rb +23 -0
- data/lib/conceptql/operators/overlaps.rb +19 -0
- data/lib/conceptql/operators/pass_thru.rb +11 -0
- data/lib/conceptql/{nodes → operators}/person.rb +8 -4
- data/lib/conceptql/operators/person_filter.rb +13 -0
- data/lib/conceptql/{nodes → operators}/place_of_service_code.rb +7 -7
- data/lib/conceptql/operators/procedure_occurrence.rb +25 -0
- data/lib/conceptql/operators/prodcode.rb +29 -0
- data/lib/conceptql/{nodes → operators}/race.rb +7 -7
- data/lib/conceptql/operators/recall.rb +38 -0
- data/lib/conceptql/operators/rxnorm.rb +24 -0
- data/lib/conceptql/operators/snomed.rb +24 -0
- data/lib/conceptql/operators/snomed_condition.rb +26 -0
- data/lib/conceptql/{nodes/source_vocabulary_node.rb → operators/source_vocabulary_operator.rb} +7 -4
- data/lib/conceptql/{nodes/standard_vocabulary_node.rb → operators/standard_vocabulary_operator.rb} +6 -4
- data/lib/conceptql/{nodes → operators}/started_by.rb +9 -3
- data/lib/conceptql/{nodes → operators}/sum.rb +9 -4
- data/lib/conceptql/{nodes/temporal_node.rb → operators/temporal_operator.rb} +7 -4
- data/lib/conceptql/{nodes → operators}/time_window.rb +19 -7
- data/lib/conceptql/operators/to_seer_visits.rb +24 -0
- data/lib/conceptql/operators/trim_date_end.rb +55 -0
- data/lib/conceptql/operators/trim_date_start.rb +56 -0
- data/lib/conceptql/{nodes → operators}/union.rb +6 -2
- data/lib/conceptql/operators/visit.rb +15 -0
- data/lib/conceptql/{nodes → operators}/visit_occurrence.rb +7 -3
- data/lib/conceptql/query.rb +19 -17
- data/lib/conceptql/scope.rb +69 -0
- data/lib/conceptql/tree.rb +33 -18
- data/lib/conceptql/utils/temp_table.rb +72 -0
- data/lib/conceptql/version.rb +1 -1
- data/spec/conceptql/behaviors/dottable_spec.rb +39 -51
- data/spec/conceptql/converter_spec.rb +51 -0
- data/spec/conceptql/date_adjuster_spec.rb +15 -15
- data/spec/conceptql/operators/after_spec.rb +16 -0
- data/spec/conceptql/operators/before_spec.rb +16 -0
- data/spec/conceptql/{nodes/casting_node_spec.rb → operators/casting_operator_spec.rb} +16 -20
- data/spec/conceptql/operators/complement_spec.rb +15 -0
- data/spec/conceptql/operators/concept_spec.rb +40 -0
- data/spec/conceptql/{nodes → operators}/condition_type_spec.rb +39 -24
- data/spec/conceptql/operators/contains_spec.rb +19 -0
- data/spec/conceptql/operators/cpt_spec.rb +29 -0
- data/spec/conceptql/operators/date_range_spec.rb +33 -0
- data/spec/conceptql/operators/death_spec.rb +10 -0
- data/spec/conceptql/operators/during_spec.rb +30 -0
- data/spec/conceptql/operators/except_spec.rb +15 -0
- data/spec/conceptql/operators/first_spec.rb +35 -0
- data/spec/conceptql/operators/from_spec.rb +13 -0
- data/spec/conceptql/operators/gender_spec.rb +27 -0
- data/spec/conceptql/operators/hcpcs_spec.rb +29 -0
- data/spec/conceptql/operators/icd10_spec.rb +34 -0
- data/spec/conceptql/operators/icd9_procedure_spec.rb +29 -0
- data/spec/conceptql/operators/icd9_spec.rb +34 -0
- data/spec/conceptql/operators/intersect_spec.rb +28 -0
- data/spec/conceptql/operators/last_spec.rb +36 -0
- data/spec/conceptql/operators/loinc_spec.rb +29 -0
- data/spec/conceptql/operators/medcode_procedure_spec.rb +34 -0
- data/spec/conceptql/operators/medcode_spec.rb +34 -0
- data/spec/conceptql/operators/observation_period_spec.rb +10 -0
- data/spec/conceptql/operators/occurrence_spec.rb +87 -0
- data/spec/conceptql/operators/overlapped_by_spec.rb +32 -0
- data/spec/conceptql/operators/overlaps_spec.rb +21 -0
- data/spec/conceptql/operators/person_filter_spec.rb +15 -0
- data/spec/conceptql/operators/person_spec.rb +10 -0
- data/spec/conceptql/{nodes → operators}/place_of_service_code_spec.rb +6 -8
- data/spec/conceptql/operators/procedure_occurrence_spec.rb +10 -0
- data/spec/conceptql/operators/prodcode_spec.rb +35 -0
- data/spec/conceptql/operators/query_double.rb +20 -0
- data/spec/conceptql/operators/query_double_spec.rb +7 -0
- data/spec/conceptql/operators/race_spec.rb +21 -0
- data/spec/conceptql/operators/rxnorm_spec.rb +29 -0
- data/spec/conceptql/operators/snomed_spec.rb +29 -0
- data/spec/conceptql/operators/source_vocabulary_operator_spec.rb +35 -0
- data/spec/conceptql/operators/standard_vocabulary_operator_spec.rb +35 -0
- data/spec/conceptql/operators/started_by_spec.rb +22 -0
- data/spec/conceptql/{nodes/temporal_node_spec.rb → operators/temporal_operator_spec.rb} +11 -17
- data/spec/conceptql/operators/time_window_spec.rb +77 -0
- data/spec/conceptql/operators/union_spec.rb +21 -0
- data/spec/conceptql/operators/visit_occurrence_spec.rb +10 -0
- data/spec/conceptql/query_spec.rb +10 -9
- data/spec/conceptql/tree_spec.rb +24 -28
- data/spec/doubles/stream_for_casting_double.rb +1 -1
- data/spec/doubles/stream_for_occurrence_double.rb +1 -1
- data/spec/doubles/stream_for_temporal_double.rb +1 -1
- data/spec/spec_helper.rb +74 -58
- metadata +202 -133
- data/lib/conceptql/nodes/after.rb +0 -12
- data/lib/conceptql/nodes/before.rb +0 -11
- data/lib/conceptql/nodes/concept.rb +0 -38
- data/lib/conceptql/nodes/count.rb +0 -23
- data/lib/conceptql/nodes/cpt.rb +0 -20
- data/lib/conceptql/nodes/death.rb +0 -19
- data/lib/conceptql/nodes/define.rb +0 -96
- data/lib/conceptql/nodes/drug_type_concept.rb +0 -18
- data/lib/conceptql/nodes/equal.rb +0 -11
- data/lib/conceptql/nodes/except.rb +0 -11
- data/lib/conceptql/nodes/hcpcs.rb +0 -20
- data/lib/conceptql/nodes/icd10.rb +0 -23
- data/lib/conceptql/nodes/icd9.rb +0 -23
- data/lib/conceptql/nodes/icd9_procedure.rb +0 -20
- data/lib/conceptql/nodes/loinc.rb +0 -20
- data/lib/conceptql/nodes/numeric.rb +0 -40
- data/lib/conceptql/nodes/occurrence.rb +0 -49
- data/lib/conceptql/nodes/pass_thru.rb +0 -11
- data/lib/conceptql/nodes/person_filter.rb +0 -12
- data/lib/conceptql/nodes/procedure_occurrence.rb +0 -21
- data/lib/conceptql/nodes/recall.rb +0 -50
- data/lib/conceptql/nodes/rxnorm.rb +0 -20
- data/lib/conceptql/nodes/snomed.rb +0 -19
- data/lib/conceptql/nodes/visit.rb +0 -11
- data/spec/conceptql/nodes/after_spec.rb +0 -18
- data/spec/conceptql/nodes/before_spec.rb +0 -18
- data/spec/conceptql/nodes/complement_spec.rb +0 -15
- data/spec/conceptql/nodes/concept_spec.rb +0 -34
- data/spec/conceptql/nodes/cpt_spec.rb +0 -31
- data/spec/conceptql/nodes/date_range_spec.rb +0 -35
- data/spec/conceptql/nodes/death_spec.rb +0 -12
- data/spec/conceptql/nodes/during_spec.rb +0 -32
- data/spec/conceptql/nodes/except_spec.rb +0 -18
- data/spec/conceptql/nodes/first_spec.rb +0 -37
- data/spec/conceptql/nodes/from_spec.rb +0 -15
- data/spec/conceptql/nodes/gender_spec.rb +0 -29
- data/spec/conceptql/nodes/hcpcs_spec.rb +0 -31
- data/spec/conceptql/nodes/icd10_spec.rb +0 -36
- data/spec/conceptql/nodes/icd9_procedure_spec.rb +0 -31
- data/spec/conceptql/nodes/icd9_spec.rb +0 -36
- data/spec/conceptql/nodes/intersect_spec.rb +0 -33
- data/spec/conceptql/nodes/last_spec.rb +0 -38
- data/spec/conceptql/nodes/loinc_spec.rb +0 -31
- data/spec/conceptql/nodes/occurrence_spec.rb +0 -89
- data/spec/conceptql/nodes/person_filter_spec.rb +0 -18
- data/spec/conceptql/nodes/person_spec.rb +0 -12
- data/spec/conceptql/nodes/procedure_occurrence_spec.rb +0 -12
- data/spec/conceptql/nodes/query_double.rb +0 -19
- data/spec/conceptql/nodes/race_spec.rb +0 -23
- data/spec/conceptql/nodes/rxnorm_spec.rb +0 -31
- data/spec/conceptql/nodes/snomed_spec.rb +0 -31
- data/spec/conceptql/nodes/source_vocabulary_node_spec.rb +0 -37
- data/spec/conceptql/nodes/standard_vocabulary_node_spec.rb +0 -40
- data/spec/conceptql/nodes/started_by_spec.rb +0 -25
- data/spec/conceptql/nodes/time_window_spec.rb +0 -85
- data/spec/conceptql/nodes/union_spec.rb +0 -25
- data/spec/conceptql/nodes/visit_occurrence_spec.rb +0 -12
data/lib/conceptql/graph.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
require 'psych'
|
|
2
2
|
require 'graphviz'
|
|
3
3
|
require_relative 'tree'
|
|
4
|
-
require_relative '
|
|
4
|
+
require_relative 'operators/operator'
|
|
5
5
|
require_relative 'behaviors/dottable'
|
|
6
6
|
|
|
7
7
|
module ConceptQL
|
|
@@ -14,16 +14,16 @@ module ConceptQL
|
|
|
14
14
|
@tree = opts.fetch(:tree, Tree.new)
|
|
15
15
|
@title = opts.fetch(:title, nil)
|
|
16
16
|
@suffix = opts.fetch(:suffix, 'pdf')
|
|
17
|
-
ConceptQL::
|
|
17
|
+
ConceptQL::Operators::Operator.send(:include, ConceptQL::Behaviors::Dottable)
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def graph_it(file_path)
|
|
21
21
|
build_graph(g)
|
|
22
|
-
|
|
22
|
+
graph.output(suffix.to_sym => file_path + ".#{suffix}")
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
def
|
|
26
|
-
@
|
|
25
|
+
def graph
|
|
26
|
+
@graph ||= begin
|
|
27
27
|
opts = { type: :digraph }
|
|
28
28
|
opts[:label] = title if title
|
|
29
29
|
GraphViz.new(:G, opts)
|
|
@@ -34,15 +34,16 @@ module ConceptQL
|
|
|
34
34
|
attr :yaml, :tree, :db
|
|
35
35
|
|
|
36
36
|
def build_graph(g)
|
|
37
|
-
tree.root(self).each.with_index do |
|
|
38
|
-
|
|
37
|
+
tree.root(self).each.with_index do |last_operator, index|
|
|
38
|
+
last_operator.build_temp_tables(db)
|
|
39
|
+
last_operator.graph_it(g, db)
|
|
39
40
|
if dangler
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
blank_operator = g.add_nodes("_#{index}")
|
|
42
|
+
blank_operator[:shape] = 'none'
|
|
43
|
+
blank_operator[:height] = 0
|
|
44
|
+
blank_operator[:label] = ''
|
|
45
|
+
blank_operator[:fixedsize] = true
|
|
46
|
+
last_operator.link_to(g, blank_operator, db)
|
|
46
47
|
end
|
|
47
48
|
end
|
|
48
49
|
end
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
require_relative 'behaviors/dottable'
|
|
2
|
-
require_relative '
|
|
2
|
+
require_relative 'operators/operator'
|
|
3
3
|
|
|
4
4
|
module ConceptQL
|
|
5
5
|
class GraphNodifier
|
|
6
|
-
class
|
|
6
|
+
class DotOperator < ConceptQL::Operators::Operator
|
|
7
7
|
include ConceptQL::Behaviors::Dottable
|
|
8
8
|
|
|
9
9
|
TYPES = {
|
|
@@ -13,6 +13,7 @@ module ConceptQL
|
|
|
13
13
|
icd9: :condition_occurrence,
|
|
14
14
|
icd10: :condition_occurrence,
|
|
15
15
|
condition_type: :condition_occurrence,
|
|
16
|
+
medcode: :condition_occurrence,
|
|
16
17
|
|
|
17
18
|
# Procedures
|
|
18
19
|
procedure: :procedure_occurrence,
|
|
@@ -21,6 +22,7 @@ module ConceptQL
|
|
|
21
22
|
hcpcs: :procedure_occurrence,
|
|
22
23
|
icd9_procedure: :procedure_occurrence,
|
|
23
24
|
procedure_cost: :procedure_cost,
|
|
25
|
+
medcode_procedure: :procedure_occurrence,
|
|
24
26
|
|
|
25
27
|
# Visits
|
|
26
28
|
visit_occurrence: :visit_occurrence,
|
|
@@ -40,6 +42,8 @@ module ConceptQL
|
|
|
40
42
|
|
|
41
43
|
# Observation
|
|
42
44
|
loinc: :observation,
|
|
45
|
+
from_seer_visits: :observation,
|
|
46
|
+
to_seer_visits: :observation,
|
|
43
47
|
|
|
44
48
|
# Drug
|
|
45
49
|
drug_exposure: :drug_exposure,
|
|
@@ -47,11 +51,12 @@ module ConceptQL
|
|
|
47
51
|
drug_cost: :drug_cost,
|
|
48
52
|
drug_type_concept_id: :drug_exposure,
|
|
49
53
|
drug_type_concept: :drug_exposure,
|
|
54
|
+
prodcode: :drug_exposure,
|
|
50
55
|
|
|
51
|
-
# Date
|
|
56
|
+
# Date Operators
|
|
52
57
|
date_range: :date,
|
|
53
58
|
|
|
54
|
-
# Miscelaneous
|
|
59
|
+
# Miscelaneous operators
|
|
55
60
|
concept: :misc,
|
|
56
61
|
vsac: :misc
|
|
57
62
|
}
|
|
@@ -78,12 +83,12 @@ module ConceptQL
|
|
|
78
83
|
end
|
|
79
84
|
|
|
80
85
|
def types
|
|
81
|
-
types = [TYPES[name.to_sym] ||
|
|
86
|
+
types = [TYPES[name.to_sym] || upstreams.map(&:types)].flatten.uniq
|
|
82
87
|
types.empty? ? [:misc] : types
|
|
83
88
|
end
|
|
84
89
|
end
|
|
85
90
|
|
|
86
|
-
class
|
|
91
|
+
class BinaryOperatorOperator < DotOperator
|
|
87
92
|
def display_name
|
|
88
93
|
output = name
|
|
89
94
|
output += "\n#{displayable_options.map{|k,v| "#{k}: #{v}"}.join("\n")}"
|
|
@@ -105,7 +110,7 @@ module ConceptQL
|
|
|
105
110
|
def graph_it(g, db)
|
|
106
111
|
left.graph_it(g, db)
|
|
107
112
|
right.graph_it(g, db)
|
|
108
|
-
cluster_name = "cluster_#{
|
|
113
|
+
cluster_name = "cluster_#{operator_name}"
|
|
109
114
|
me = g.send(cluster_name) do |sub|
|
|
110
115
|
sub[rank: 'same', label: display_name, color: 'black']
|
|
111
116
|
sub.send("#{cluster_name}_left").send('[]', shape: 'point', color: type_color(types))
|
|
@@ -113,7 +118,7 @@ module ConceptQL
|
|
|
113
118
|
end
|
|
114
119
|
left.link_to(g, me.send("#{cluster_name}_left"))
|
|
115
120
|
right.link_to(g, me.send("#{cluster_name}_right"))
|
|
116
|
-
@
|
|
121
|
+
@__graph_operator = me.send("#{cluster_name}_left")
|
|
117
122
|
end
|
|
118
123
|
|
|
119
124
|
def types
|
|
@@ -125,19 +130,37 @@ module ConceptQL
|
|
|
125
130
|
end
|
|
126
131
|
end
|
|
127
132
|
|
|
128
|
-
class
|
|
133
|
+
class LetOperator < DotOperator
|
|
134
|
+
def graph_it(g, db)
|
|
135
|
+
cluster_name = "cluster_#{operator_name}"
|
|
136
|
+
linkable = nil
|
|
137
|
+
g.send(cluster_name) do |sub|
|
|
138
|
+
linkable = upstreams.reverse.map do |upstream|
|
|
139
|
+
upstream.graph_it(sub, db)
|
|
140
|
+
end.first
|
|
141
|
+
sub[label: display_name, color: 'black']
|
|
142
|
+
end
|
|
143
|
+
@__graph_operator = linkable
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def types
|
|
147
|
+
upstreams.last.types
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
class DefineOperator < DotOperator
|
|
129
152
|
def shape
|
|
130
153
|
:cds
|
|
131
154
|
end
|
|
132
155
|
end
|
|
133
156
|
|
|
134
|
-
class
|
|
157
|
+
class RecallOperator < DotOperator
|
|
135
158
|
def shape
|
|
136
159
|
:cds
|
|
137
160
|
end
|
|
138
161
|
end
|
|
139
162
|
|
|
140
|
-
class
|
|
163
|
+
class VsacOperator < DotOperator
|
|
141
164
|
def initialize(name, values, types)
|
|
142
165
|
@types = types
|
|
143
166
|
super(name, values)
|
|
@@ -150,22 +173,31 @@ module ConceptQL
|
|
|
150
173
|
|
|
151
174
|
BINARY_OPERATOR_TYPES = %w(before after meets met_by started_by starts contains during overlaps overlapped_by finished_by finishes coincides except person_filter less_than less_than_or_equal equal not_equal greater_than greater_than_or_equal filter).map { |temp| [temp, "not_#{temp}"] }.flatten.map(&:to_sym)
|
|
152
175
|
|
|
176
|
+
def temp_tables
|
|
177
|
+
@temp_tables ||= {}
|
|
178
|
+
end
|
|
179
|
+
|
|
153
180
|
def types
|
|
154
181
|
@types ||= {}
|
|
155
182
|
end
|
|
156
183
|
|
|
157
184
|
def create(type, values, tree)
|
|
158
|
-
if BINARY_OPERATOR_TYPES.include?(type)
|
|
159
|
-
|
|
185
|
+
operator = if BINARY_OPERATOR_TYPES.include?(type)
|
|
186
|
+
BinaryOperatorOperator.new(type, values)
|
|
187
|
+
elsif type == :let
|
|
188
|
+
LetOperator.new(type, values)
|
|
160
189
|
elsif type == :define
|
|
161
|
-
|
|
190
|
+
DefineOperator.new(type, values)
|
|
162
191
|
elsif type == :recall
|
|
163
|
-
|
|
192
|
+
RecallOperator.new(type, values)
|
|
164
193
|
elsif type == :vsac
|
|
165
194
|
types = values.pop
|
|
166
|
-
|
|
195
|
+
VsacOperator.new(type, values, types)
|
|
196
|
+
else
|
|
197
|
+
DotOperator.new(type, values)
|
|
167
198
|
end
|
|
168
|
-
|
|
199
|
+
operator.tree = self
|
|
200
|
+
operator
|
|
169
201
|
end
|
|
170
202
|
end
|
|
171
203
|
end
|
data/lib/conceptql/nodifier.rb
CHANGED
|
@@ -1,11 +1,36 @@
|
|
|
1
|
-
require '
|
|
1
|
+
require 'facets/hash/deep_rekey'
|
|
2
|
+
require 'facets/pathname/chdir'
|
|
3
|
+
require 'facets/string/modulize'
|
|
4
|
+
|
|
2
5
|
module ConceptQL
|
|
3
6
|
class Nodifier
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
attr_reader :operators
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@operators = {}
|
|
11
|
+
dir = Pathname.new(__FILE__).dirname()
|
|
12
|
+
dir.chdir do
|
|
13
|
+
Pathname.glob("operators/*.rb").each do |file|
|
|
14
|
+
require_relative file
|
|
15
|
+
operator = file.basename('.*').to_s.to_sym
|
|
16
|
+
klass = Object.const_get("conceptQL/operators/#{operator}".modulize)
|
|
17
|
+
@operators[operator] = klass
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def create(scope, operator, *values)
|
|
23
|
+
operator = operator.to_sym
|
|
24
|
+
if operators[operator].nil?
|
|
25
|
+
raise "Can't find operator for '#{operator}' in #{operators.keys.sort}"
|
|
26
|
+
end
|
|
27
|
+
operator = operators[operator].new(*values)
|
|
28
|
+
operator.scope = scope
|
|
29
|
+
operator
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def to_metadata
|
|
33
|
+
Hash[operators.map { |k, v| [k, v.to_metadata]}.select { |k, v| v[:desc] }]
|
|
9
34
|
end
|
|
10
35
|
end
|
|
11
36
|
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require_relative 'temporal_operator'
|
|
2
|
+
|
|
3
|
+
module ConceptQL
|
|
4
|
+
module Operators
|
|
5
|
+
class After < TemporalOperator
|
|
6
|
+
desc <<-EOF
|
|
7
|
+
Compares all results on a person-by-person basis between the left hand results (LHR) and the right hand resuls (RHR).
|
|
8
|
+
For any result in the LHR whose start_date occurs after the earliest end_date of the RHR, that result is passed through.
|
|
9
|
+
All other results are discarded, including all results in the RHR.
|
|
10
|
+
L-------N-------L
|
|
11
|
+
R-----R
|
|
12
|
+
R-----R
|
|
13
|
+
L-----Y----L
|
|
14
|
+
EOF
|
|
15
|
+
def right_stream(db)
|
|
16
|
+
right.evaluate(db).from_self.group_by(:person_id).select(:person_id, Sequel.function(:min, :end_date).as(:end_date)).as(:r)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def where_clause
|
|
20
|
+
Proc.new { l__start_date > r__end_date }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
require_relative 'temporal_operator'
|
|
2
|
+
|
|
3
|
+
module ConceptQL
|
|
4
|
+
module Operators
|
|
5
|
+
class AnyOverlap < TemporalOperator
|
|
6
|
+
desc 'If a result from the LHR overlaps in any way a result from the RHR it is passed along.'
|
|
7
|
+
def where_clause
|
|
8
|
+
l_partly_in_r = Sequel.expr { r__start_date <= l__start_date }.&(Sequel.expr { l__start_date <= r__end_date })
|
|
9
|
+
r_partly_in_l = Sequel.expr { l__start_date <= r__start_date }.&(Sequel.expr { r__start_date <= l__end_date })
|
|
10
|
+
l_partly_in_r.|(r_partly_in_l)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
require_relative 'temporal_operator'
|
|
2
|
+
|
|
3
|
+
module ConceptQL
|
|
4
|
+
module Operators
|
|
5
|
+
class Before < TemporalOperator
|
|
6
|
+
desc <<-EOF
|
|
7
|
+
Compares all results on a person-by-person basis between the left hand results (LHR) and the right hand resuls (RHR).
|
|
8
|
+
For any result in the LHR whose end_date occurs before the most recent start_date of the RHR, that result is passed through.
|
|
9
|
+
All other results are discarded, including all results in the RHR.
|
|
10
|
+
EOF
|
|
11
|
+
|
|
12
|
+
def right_stream(db)
|
|
13
|
+
right.evaluate(db).from_self.group_by(:person_id).select(:person_id, Sequel.function(:max, :start_date).as(:start_date)).as(:r)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def where_clause
|
|
17
|
+
Proc.new { l__end_date < r__start_date }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -1,17 +1,21 @@
|
|
|
1
|
-
require_relative '
|
|
1
|
+
require_relative 'operator'
|
|
2
|
+
require 'facets/string/titlecase'
|
|
2
3
|
|
|
3
4
|
module ConceptQL
|
|
4
|
-
module
|
|
5
|
-
# Base class for all
|
|
6
|
-
class
|
|
7
|
-
|
|
5
|
+
module Operators
|
|
6
|
+
# Base class for all operators that take two streams, a left-hand and a right-hand
|
|
7
|
+
class BinaryOperatorOperator < Operator
|
|
8
|
+
option :left, type: :upstream
|
|
9
|
+
option :right, type: :upstream
|
|
10
|
+
|
|
11
|
+
def upstreams
|
|
8
12
|
[left]
|
|
9
13
|
end
|
|
10
14
|
|
|
11
15
|
def graph_it(g, db)
|
|
12
16
|
left.graph_it(g, db)
|
|
13
17
|
right.graph_it(g, db)
|
|
14
|
-
cluster_name = "cluster_#{
|
|
18
|
+
cluster_name = "cluster_#{operator_name}"
|
|
15
19
|
me = g.send(cluster_name) do |sub|
|
|
16
20
|
sub[rank: 'same', label: display_name, color: 'black']
|
|
17
21
|
sub.send("#{cluster_name}_left").send('[]', shape: 'point', color: type_color(types))
|
|
@@ -19,11 +23,11 @@ module ConceptQL
|
|
|
19
23
|
end
|
|
20
24
|
left.link_to(g, me.send("#{cluster_name}_left"), db)
|
|
21
25
|
right.link_to(g, me.send("#{cluster_name}_right"), db)
|
|
22
|
-
@
|
|
26
|
+
@__graph_operator = me.send("#{cluster_name}_left")
|
|
23
27
|
end
|
|
24
28
|
|
|
25
29
|
def display_name
|
|
26
|
-
self.class.name.split('::').last.
|
|
30
|
+
self.class.name.split('::').last.snakecase.titlecase
|
|
27
31
|
end
|
|
28
32
|
|
|
29
33
|
private
|
|
@@ -1,28 +1,29 @@
|
|
|
1
|
-
require_relative '
|
|
1
|
+
require_relative 'operator'
|
|
2
2
|
|
|
3
3
|
module ConceptQL
|
|
4
|
-
module
|
|
5
|
-
# Parent class of all casting
|
|
4
|
+
module Operators
|
|
5
|
+
# Parent class of all casting operators
|
|
6
6
|
#
|
|
7
7
|
# Subclasses must implement the following methods:
|
|
8
8
|
# - my_type
|
|
9
9
|
# - i_point_at
|
|
10
10
|
# - these_point_at_me
|
|
11
11
|
#
|
|
12
|
-
# i_point_at returns a list of types for which the
|
|
12
|
+
# i_point_at returns a list of types for which the operator's table of origin
|
|
13
13
|
# has foreign_keys pointing to another table, e.g.:
|
|
14
14
|
# procedure_occurrence has an FK to visit_occurrence, so we'd put
|
|
15
15
|
# :visit_occurrence in the i_point_at array
|
|
16
16
|
#
|
|
17
17
|
# these_point_at_me is a list of types for which that type's table
|
|
18
|
-
# of origin has a FK pointing to the current
|
|
18
|
+
# of origin has a FK pointing to the current operator's
|
|
19
19
|
# table of origin, e.g.:
|
|
20
20
|
# procedure_cost has an FK to procedure_occurrence so we'd
|
|
21
21
|
# put :procedure_cost in procedure_occurrence's these_point_at_me array
|
|
22
22
|
#
|
|
23
|
-
# Also, if a casting
|
|
23
|
+
# Also, if a casting operator is passed no streams, it will return all the
|
|
24
24
|
# rows in its table as results.
|
|
25
|
-
class
|
|
25
|
+
class CastingOperator < Operator
|
|
26
|
+
category 'Casting'
|
|
26
27
|
def types
|
|
27
28
|
[type]
|
|
28
29
|
end
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
require_relative 'pass_thru'
|
|
2
2
|
|
|
3
3
|
module ConceptQL
|
|
4
|
-
module
|
|
4
|
+
module Operators
|
|
5
5
|
class Complement < PassThru
|
|
6
|
+
desc 'Splits up the incoming result set by type and passes through all results for each type that are NOT in the current set.'
|
|
7
|
+
allows_one_upstream
|
|
8
|
+
category 'Set Logic'
|
|
9
|
+
|
|
6
10
|
def query(db)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
positive_query = db.from(
|
|
11
|
+
upstream = upstreams.first
|
|
12
|
+
upstream.types.map do |type|
|
|
13
|
+
positive_query = db.from(upstream.evaluate(db))
|
|
10
14
|
.select(:criterion_id)
|
|
11
15
|
.exclude(:criterion_id => nil)
|
|
12
16
|
.where(:criterion_type => type.to_s)
|
|
@@ -18,18 +22,17 @@ module ConceptQL
|
|
|
18
22
|
end
|
|
19
23
|
end
|
|
20
24
|
|
|
21
|
-
|
|
22
|
-
This is an alternate, but equally accurate way to do complement.
|
|
23
|
-
We'll need to benchmark which is faster.
|
|
25
|
+
|
|
26
|
+
# This is an alternate, but equally accurate way to do complement.
|
|
27
|
+
# We'll need to benchmark which is faster.
|
|
24
28
|
def query2(db)
|
|
25
|
-
|
|
26
|
-
froms =
|
|
29
|
+
upstream = upstreams.first
|
|
30
|
+
froms = upstream.types.map do |type|
|
|
27
31
|
select_it(db.from(make_table_name(type)), type)
|
|
28
32
|
end
|
|
29
33
|
big_from = froms.inject { |union_query, q| union_query.union(q, all:true) }
|
|
30
|
-
db.from(big_from).except(
|
|
34
|
+
db.from(big_from).except(upstream.evaluate(db))
|
|
31
35
|
end
|
|
32
|
-
=end
|
|
33
36
|
end
|
|
34
37
|
end
|
|
35
38
|
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
require_relative 'operator'
|
|
2
|
+
require_relative '../query'
|
|
3
|
+
|
|
4
|
+
module ConceptQL
|
|
5
|
+
module Operators
|
|
6
|
+
class Concept < Operator
|
|
7
|
+
desc 'Given the UUID of another ConceptQL statement, returns the results of that statement.'
|
|
8
|
+
attr :cql_query
|
|
9
|
+
def query(db)
|
|
10
|
+
set_cql_query(db)
|
|
11
|
+
db.from(cql_query.query.from_self)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def upstreams
|
|
15
|
+
@upstreams ||= cql_query.operators
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def graph_prep(db)
|
|
19
|
+
set_cql_query(db)
|
|
20
|
+
puts statement.inspect
|
|
21
|
+
@arguments = [description(db)]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def print_prep(db)
|
|
25
|
+
set_cql_query(db)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def ensure_temp_tables(db)
|
|
29
|
+
set_cql_query(db)
|
|
30
|
+
super
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def build_temp_tables(db)
|
|
34
|
+
set_cql_query(db)
|
|
35
|
+
super
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def set_cql_query(db)
|
|
41
|
+
@cql_query ||= begin
|
|
42
|
+
set_statement(db)
|
|
43
|
+
Query.new(db, statement)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def statement
|
|
48
|
+
raise "Statement is nil!" unless @statement
|
|
49
|
+
@statement
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def set_statement(db)
|
|
53
|
+
@statement ||= get_statement(db)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def get_statement(db)
|
|
57
|
+
concept = db[:concepts].where(concept_id: arguments.first).limit(1)
|
|
58
|
+
statement = concept.select_map(:statement).first.tap { |f| ConceptQL.logger.debug f.inspect }
|
|
59
|
+
statement = JSON.parse(statement) if statement.is_a?(String)
|
|
60
|
+
statement
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def description(db)
|
|
64
|
+
@description ||= db[:concepts].where(concept_id: arguments.first).select_map(:label).first
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
|
|
@@ -1,15 +1,20 @@
|
|
|
1
|
-
require_relative '
|
|
1
|
+
require_relative 'operator'
|
|
2
2
|
|
|
3
3
|
module ConceptQL
|
|
4
|
-
module
|
|
5
|
-
# Represents a
|
|
4
|
+
module Operators
|
|
5
|
+
# Represents a operator that will grab all conditions that match the
|
|
6
6
|
# condition type passed in
|
|
7
7
|
#
|
|
8
8
|
# Condition Type represents which position the condition held in
|
|
9
9
|
# the raw data, e.g. primary inpatient header or 15th outpatient detail
|
|
10
10
|
#
|
|
11
11
|
# Multiple types can be specified at once
|
|
12
|
-
class ConditionType <
|
|
12
|
+
class ConditionType < Operator
|
|
13
|
+
desc 'Searches for conditions that match the given set of Condition Types'
|
|
14
|
+
argument :condition_types, type: :codelist, vocab: 'Condition Type'
|
|
15
|
+
category %(Occurrence Type)
|
|
16
|
+
predominant_types :condition_occurrence
|
|
17
|
+
|
|
13
18
|
def type
|
|
14
19
|
:condition_occurrence
|
|
15
20
|
end
|
|
@@ -29,7 +34,7 @@ module ConceptQL
|
|
|
29
34
|
def to_concept_id(ctype)
|
|
30
35
|
ctype = ctype.to_s.downcase
|
|
31
36
|
position = nil
|
|
32
|
-
if ctype =~ /(\d|_primary)$/
|
|
37
|
+
if ctype =~ /(\d|_primary)$/ && ctype.count('_') > 1
|
|
33
38
|
parts = ctype.split('_')
|
|
34
39
|
position = parts.pop.to_i
|
|
35
40
|
position -= 1 if ctype =~ /^outpatient/
|
|
@@ -51,6 +56,9 @@ module ConceptQL
|
|
|
51
56
|
condition_era_0_day_window: [38000246],
|
|
52
57
|
condition_era_30_day_window: [38000247]
|
|
53
58
|
}
|
|
59
|
+
hash[:primary] = %w(inpatient_detail inpatient_header outpatient_detail outpatient_header).map { |w| hash[w.to_sym].first }
|
|
60
|
+
hash[:outpatient_primary] = %w(outpatient_detail outpatient_header).map { |w| hash[w.to_sym].first }
|
|
61
|
+
hash[:inpatient_primary] = %w(inpatient_detail inpatient_header).map { |w| hash[w.to_sym].first }
|
|
54
62
|
hash[:inpatient] = hash[:inpatient_detail] + hash[:inpatient_header]
|
|
55
63
|
hash[:outpatient] = hash[:outpatient_detail] + hash[:outpatient_header]
|
|
56
64
|
hash[:condition_era] = hash[:condition_era_0_day_window] + hash[:condition_era_30_day_window]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
require_relative 'temporal_operator'
|
|
2
|
+
|
|
3
|
+
module ConceptQL
|
|
4
|
+
module Operators
|
|
5
|
+
class Contains < TemporalOperator
|
|
6
|
+
desc <<-EOF
|
|
7
|
+
Any result in the LHR whose start_date is on or before and whose end_date is on or after a result from the RHR.
|
|
8
|
+
L--X-L
|
|
9
|
+
R-----R
|
|
10
|
+
L------Y--------L
|
|
11
|
+
EOF
|
|
12
|
+
def where_clause
|
|
13
|
+
[Proc.new { l__start_date <= r__start_date}, Proc.new { r__end_date <= l__end_date }]
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
require_relative 'pass_thru'
|
|
2
|
+
|
|
3
|
+
module ConceptQL
|
|
4
|
+
module Operators
|
|
5
|
+
class Count < PassThru
|
|
6
|
+
desc 'Counts the number of results the exactly match across all columns.'
|
|
7
|
+
allows_one_upstream
|
|
8
|
+
|
|
9
|
+
def query(db)
|
|
10
|
+
db.from(unioned(db))
|
|
11
|
+
.group(*COLUMNS)
|
|
12
|
+
.select(*(COLUMNS - [:value_as_number]))
|
|
13
|
+
.select_append{count(1).as(:value_as_number)}
|
|
14
|
+
.from_self
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def unioned(db)
|
|
18
|
+
upstreams.map { |c| c.evaluate(db) }.inject do |uni, q|
|
|
19
|
+
uni.union(q)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require_relative 'standard_vocabulary_operator'
|
|
2
|
+
|
|
3
|
+
module ConceptQL
|
|
4
|
+
module Operators
|
|
5
|
+
class Cpt < StandardVocabularyOperator
|
|
6
|
+
preferred_name 'CPT'
|
|
7
|
+
desc 'Searches the procedure_occurrence table for all procedures with matching CPT codes'
|
|
8
|
+
argument :cpts, type: :codelist, vocab: 'CPT4'
|
|
9
|
+
predominant_types :procedure_occurrence
|
|
10
|
+
|
|
11
|
+
def table
|
|
12
|
+
:procedure_occurrence
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def vocabulary_id
|
|
16
|
+
4
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def concept_column
|
|
20
|
+
:procedure_concept_id
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
require_relative '
|
|
1
|
+
require_relative 'operator'
|
|
2
2
|
|
|
3
3
|
module ConceptQL
|
|
4
|
-
module
|
|
5
|
-
# Represents a
|
|
4
|
+
module Operators
|
|
5
|
+
# Represents a operator that will create a date_range for every person in the database
|
|
6
6
|
#
|
|
7
7
|
# Accepts two params: start and end formateed as 'YYYY-MM-DD' or 'START' or 'END'
|
|
8
8
|
# 'START' represents the first date of data in the data source,
|
|
9
9
|
# 'END' represents the last date of data in the data source,
|
|
10
|
-
class DateRange <
|
|
10
|
+
class DateRange < Operator
|
|
11
|
+
desc 'Used to represent a date literal.'
|
|
12
|
+
option :start, type: :string
|
|
13
|
+
option :end, type: :string
|
|
14
|
+
category %(Temporal Manipulation)
|
|
15
|
+
|
|
11
16
|
def query(db)
|
|
12
17
|
db.from(:person)
|
|
13
18
|
.select_append(Sequel.cast_string('person').as(:criterion_type))
|
|
@@ -31,8 +36,8 @@ module ConceptQL
|
|
|
31
36
|
# TODO: Select the earliest and latest dates of observation from
|
|
32
37
|
# the proper CDM table to represent the start and end of data
|
|
33
38
|
def date_from(db, str)
|
|
34
|
-
return db.from(:
|
|
35
|
-
return db.from(:
|
|
39
|
+
return db.from(:observation_period).select { min(:observation_period_start_date) } if str.upcase == 'START'
|
|
40
|
+
return db.from(:observation_period).select { max(:observation_period_end_date) } if str.upcase == 'END'
|
|
36
41
|
return Sequel.lit('CONVERT(DATETIME, ?)', str) if db.database_type == :mssql
|
|
37
42
|
return Sequel.lit('date ?', str)
|
|
38
43
|
end
|