conceptql 0.0.1
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 +7 -0
- data/.gitignore +22 -0
- data/CHANGELOG.md +17 -0
- data/Gemfile +4 -0
- data/Guardfile +28 -0
- data/LICENSE.txt +22 -0
- data/README.md +108 -0
- data/Rakefile +1 -0
- data/bin/conceptql +5 -0
- data/conceptql.gemspec +30 -0
- data/doc/ConceptQL Specification (alpha).pdf +0 -0
- data/doc/diagram_0.png +0 -0
- data/doc/spec.md +1208 -0
- data/lib/conceptql/behaviors/dottable.rb +71 -0
- data/lib/conceptql/cli.rb +135 -0
- data/lib/conceptql/date_adjuster.rb +45 -0
- data/lib/conceptql/graph.rb +49 -0
- data/lib/conceptql/graph_nodifier.rb +123 -0
- data/lib/conceptql/logger.rb +10 -0
- data/lib/conceptql/nodes/after.rb +12 -0
- data/lib/conceptql/nodes/before.rb +11 -0
- data/lib/conceptql/nodes/binary_operator_node.rb +41 -0
- data/lib/conceptql/nodes/casting_node.rb +75 -0
- data/lib/conceptql/nodes/complement.rb +16 -0
- data/lib/conceptql/nodes/concept.rb +38 -0
- data/lib/conceptql/nodes/condition_type.rb +63 -0
- data/lib/conceptql/nodes/cpt.rb +20 -0
- data/lib/conceptql/nodes/date_range.rb +39 -0
- data/lib/conceptql/nodes/death.rb +19 -0
- data/lib/conceptql/nodes/during.rb +16 -0
- data/lib/conceptql/nodes/except.rb +11 -0
- data/lib/conceptql/nodes/first.rb +24 -0
- data/lib/conceptql/nodes/from.rb +15 -0
- data/lib/conceptql/nodes/gender.rb +27 -0
- data/lib/conceptql/nodes/hcpcs.rb +20 -0
- data/lib/conceptql/nodes/icd10.rb +23 -0
- data/lib/conceptql/nodes/icd9.rb +23 -0
- data/lib/conceptql/nodes/icd9_procedure.rb +20 -0
- data/lib/conceptql/nodes/intersect.rb +29 -0
- data/lib/conceptql/nodes/last.rb +24 -0
- data/lib/conceptql/nodes/loinc.rb +20 -0
- data/lib/conceptql/nodes/node.rb +71 -0
- data/lib/conceptql/nodes/occurrence.rb +47 -0
- data/lib/conceptql/nodes/pass_thru.rb +11 -0
- data/lib/conceptql/nodes/person.rb +25 -0
- data/lib/conceptql/nodes/person_filter.rb +12 -0
- data/lib/conceptql/nodes/place_of_service_code.rb +23 -0
- data/lib/conceptql/nodes/procedure_occurrence.rb +21 -0
- data/lib/conceptql/nodes/race.rb +23 -0
- data/lib/conceptql/nodes/rxnorm.rb +20 -0
- data/lib/conceptql/nodes/snomed.rb +19 -0
- data/lib/conceptql/nodes/source_vocabulary_node.rb +54 -0
- data/lib/conceptql/nodes/standard_vocabulary_node.rb +43 -0
- data/lib/conceptql/nodes/started_by.rb +16 -0
- data/lib/conceptql/nodes/temporal_node.rb +25 -0
- data/lib/conceptql/nodes/time_window.rb +54 -0
- data/lib/conceptql/nodes/union.rb +15 -0
- data/lib/conceptql/nodes/visit.rb +11 -0
- data/lib/conceptql/nodes/visit_occurrence.rb +26 -0
- data/lib/conceptql/nodifier.rb +9 -0
- data/lib/conceptql/query.rb +39 -0
- data/lib/conceptql/tree.rb +36 -0
- data/lib/conceptql/version.rb +3 -0
- data/lib/conceptql/view_maker.rb +56 -0
- data/lib/conceptql.rb +7 -0
- data/spec/conceptql/behaviors/dottable_spec.rb +111 -0
- data/spec/conceptql/date_adjuster_spec.rb +68 -0
- data/spec/conceptql/nodes/after_spec.rb +18 -0
- data/spec/conceptql/nodes/before_spec.rb +18 -0
- data/spec/conceptql/nodes/casting_node_spec.rb +73 -0
- data/spec/conceptql/nodes/complement_spec.rb +15 -0
- data/spec/conceptql/nodes/concept_spec.rb +34 -0
- data/spec/conceptql/nodes/condition_type_spec.rb +113 -0
- data/spec/conceptql/nodes/cpt_spec.rb +31 -0
- data/spec/conceptql/nodes/date_range_spec.rb +35 -0
- data/spec/conceptql/nodes/death_spec.rb +12 -0
- data/spec/conceptql/nodes/during_spec.rb +32 -0
- data/spec/conceptql/nodes/except_spec.rb +18 -0
- data/spec/conceptql/nodes/first_spec.rb +37 -0
- data/spec/conceptql/nodes/from_spec.rb +15 -0
- data/spec/conceptql/nodes/gender_spec.rb +29 -0
- data/spec/conceptql/nodes/hcpcs_spec.rb +31 -0
- data/spec/conceptql/nodes/icd10_spec.rb +36 -0
- data/spec/conceptql/nodes/icd9_procedure_spec.rb +31 -0
- data/spec/conceptql/nodes/icd9_spec.rb +36 -0
- data/spec/conceptql/nodes/intersect_spec.rb +33 -0
- data/spec/conceptql/nodes/last_spec.rb +38 -0
- data/spec/conceptql/nodes/loinc_spec.rb +31 -0
- data/spec/conceptql/nodes/occurrence_spec.rb +89 -0
- data/spec/conceptql/nodes/person_filter_spec.rb +18 -0
- data/spec/conceptql/nodes/person_spec.rb +12 -0
- data/spec/conceptql/nodes/place_of_service_code_spec.rb +26 -0
- data/spec/conceptql/nodes/procedure_occurrence_spec.rb +12 -0
- data/spec/conceptql/nodes/query_double.rb +19 -0
- data/spec/conceptql/nodes/race_spec.rb +23 -0
- data/spec/conceptql/nodes/rxnorm_spec.rb +31 -0
- data/spec/conceptql/nodes/snomed_spec.rb +31 -0
- data/spec/conceptql/nodes/source_vocabulary_node_spec.rb +37 -0
- data/spec/conceptql/nodes/standard_vocabulary_node_spec.rb +40 -0
- data/spec/conceptql/nodes/started_by_spec.rb +25 -0
- data/spec/conceptql/nodes/temporal_node_spec.rb +57 -0
- data/spec/conceptql/nodes/time_window_spec.rb +66 -0
- data/spec/conceptql/nodes/union_spec.rb +25 -0
- data/spec/conceptql/nodes/visit_occurrence_spec.rb +12 -0
- data/spec/conceptql/query_spec.rb +20 -0
- data/spec/conceptql/tree_spec.rb +54 -0
- data/spec/doubles/stream_for_casting_double.rb +9 -0
- data/spec/doubles/stream_for_occurrence_double.rb +21 -0
- data/spec/doubles/stream_for_temporal_double.rb +6 -0
- data/spec/spec_helper.rb +74 -0
- metadata +327 -0
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'active_support/inflector'
|
2
|
+
module ConceptQL
|
3
|
+
module Behaviors
|
4
|
+
module Dottable
|
5
|
+
@@counter = 0
|
6
|
+
TYPE_COLORS = {
|
7
|
+
person: 'blue',
|
8
|
+
visit_occurrence: 'orange',
|
9
|
+
condition_occurrence: 'red',
|
10
|
+
procedure_occurrence: 'green3',
|
11
|
+
procedure_cost: 'gold',
|
12
|
+
death: 'brown',
|
13
|
+
payer_plan_period: 'blue',
|
14
|
+
drug_exposure: 'purple',
|
15
|
+
observation: 'magenta',
|
16
|
+
misc: 'black'
|
17
|
+
}
|
18
|
+
def node_name
|
19
|
+
@__node_name ||= self.class.name.split('::').last.underscore.gsub(/\W/, '_').downcase + '_' + (@@counter += 1).to_s
|
20
|
+
end
|
21
|
+
|
22
|
+
def display_name
|
23
|
+
@__display_name ||= begin
|
24
|
+
output = self.class.name.split('::').last.titleize
|
25
|
+
output += ": #{arguments.join(', ')}" unless arguments.empty?
|
26
|
+
if output.length > 100
|
27
|
+
parts = output.split
|
28
|
+
output = parts.each_slice(output.length / parts.count).map do |subparts|
|
29
|
+
subparts.join(' ')
|
30
|
+
end.join ('\n')
|
31
|
+
end
|
32
|
+
output += "\n#{options.map{|k,v| "#{k}: #{v}"}.join("\n")}" unless options.nil? || options.empty?
|
33
|
+
output
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def type_color(*types)
|
38
|
+
types.flatten!
|
39
|
+
types.length == 1 ? TYPE_COLORS[types.first] || 'black' : 'black'
|
40
|
+
end
|
41
|
+
|
42
|
+
def graph_node(g)
|
43
|
+
@__graph_node ||= begin
|
44
|
+
me = g.add_nodes(node_name)
|
45
|
+
me[:label] = display_name
|
46
|
+
me[:color] = type_color(types)
|
47
|
+
me
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def link_to(g, dest_node)
|
52
|
+
types.each do |type|
|
53
|
+
e = g.add_edges(graph_node(g), dest_node)
|
54
|
+
e[:color] = type_color(type)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def graph_it(g, db)
|
59
|
+
graph_prep(db) if respond_to?(:graph_prep)
|
60
|
+
children.each do |child|
|
61
|
+
child.graph_it(g, db)
|
62
|
+
end
|
63
|
+
graph_node(g)
|
64
|
+
children.each do |child|
|
65
|
+
child.link_to(g, graph_node(g))
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
@@ -0,0 +1,135 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'sequelizer'
|
3
|
+
require 'psych'
|
4
|
+
require 'json'
|
5
|
+
require 'pp'
|
6
|
+
require_relative 'query'
|
7
|
+
|
8
|
+
module ConceptQL
|
9
|
+
class CLI < Thor
|
10
|
+
include Sequelizer
|
11
|
+
|
12
|
+
class_option :adapter,
|
13
|
+
aliases: :a,
|
14
|
+
desc: 'adapter for database'
|
15
|
+
class_option :host,
|
16
|
+
aliases: :h,
|
17
|
+
banner: 'localhost',
|
18
|
+
desc: 'host for database'
|
19
|
+
class_option :username,
|
20
|
+
aliases: :u,
|
21
|
+
desc: 'username for database'
|
22
|
+
class_option :password,
|
23
|
+
aliases: :P,
|
24
|
+
desc: 'password for database'
|
25
|
+
class_option :port,
|
26
|
+
aliases: :p,
|
27
|
+
type: :numeric,
|
28
|
+
banner: '5432',
|
29
|
+
desc: 'port for database'
|
30
|
+
class_option :database,
|
31
|
+
aliases: :d,
|
32
|
+
desc: 'database for database'
|
33
|
+
class_option :search_path,
|
34
|
+
aliases: :s,
|
35
|
+
desc: 'schema for database (PostgreSQL only)'
|
36
|
+
|
37
|
+
desc 'run_statement statement_file', 'Evals the statement from the statement file and executes it agains the DB specified by DB_URL'
|
38
|
+
def run_statement(statement_file)
|
39
|
+
q = ConceptQL::Query.new(db(options), criteria_from_file(statement_file))
|
40
|
+
puts q.query.sql
|
41
|
+
puts q.statement.to_yaml
|
42
|
+
pp q.execute
|
43
|
+
end
|
44
|
+
|
45
|
+
desc 'fake_graph file', 'Evals the file and shows the contents as a ConceptQL graph'
|
46
|
+
def fake_graph(file)
|
47
|
+
require_relative 'graph'
|
48
|
+
require_relative 'tree'
|
49
|
+
require_relative 'graph_nodifier'
|
50
|
+
ConceptQL::Graph.new(criteria_from_file(file),
|
51
|
+
dangler: true,
|
52
|
+
tree: ConceptQL::Tree.new(nodifier: ConceptQL::GraphNodifier.new)
|
53
|
+
).graph_it('/tmp/graph')
|
54
|
+
system('open /tmp/graph.pdf')
|
55
|
+
end
|
56
|
+
|
57
|
+
desc 'show_graph file', 'Evals the file and shows the contents as a ConceptQL graph'
|
58
|
+
def show_graph(file)
|
59
|
+
graph_it(criteria_from_file(file))
|
60
|
+
end
|
61
|
+
|
62
|
+
desc 'show_and_tell_file file', 'Evals the file and shows the contents as a ConceptQL graph, then executes the statement against our test database'
|
63
|
+
option :full
|
64
|
+
def show_and_tell_file(file)
|
65
|
+
show_and_tell(criteria_from_file(file), options)
|
66
|
+
end
|
67
|
+
|
68
|
+
desc 'show_and_tell_db conceptql_id', 'Fetches the ConceptQL from a DB and shows the contents as a ConceptQL graph, then executes the statement against our test database'
|
69
|
+
option :full
|
70
|
+
def show_and_tell_db(conceptql_id)
|
71
|
+
result = fetch_conceptql(conceptql_id, options)
|
72
|
+
puts "Concept: #{result[:label]}"
|
73
|
+
show_and_tell(result[:statement].to_hash, options, result[:label])
|
74
|
+
end
|
75
|
+
|
76
|
+
desc 'show_db_graph conceptql_id', 'Shows a graph for the conceptql statement represented by conceptql_id in the db specified by db_url'
|
77
|
+
def show_db_graph(conceptql_id)
|
78
|
+
result = fetch_conceptql(conceptql_id, options)
|
79
|
+
graph_it(result[:statement].to_hash, db, result[:label])
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
def fetch_conceptql(conceptql_id)
|
84
|
+
my_db = db(options)
|
85
|
+
my_db.extension(:pg_array, :pg_json)
|
86
|
+
my_db[:concepts].where(concept_id: conceptql_id).select(:statement, :label).first
|
87
|
+
end
|
88
|
+
|
89
|
+
def show_and_tell(statement, options, title = nil)
|
90
|
+
my_db = db(options)
|
91
|
+
q = ConceptQL::Query.new(my_db, statement)
|
92
|
+
puts 'YAML'
|
93
|
+
puts q.statement.to_yaml
|
94
|
+
puts 'JSON'
|
95
|
+
puts JSON.pretty_generate(q.statement)
|
96
|
+
STDIN.gets
|
97
|
+
graph_it(statement, my_db, title)
|
98
|
+
STDIN.gets
|
99
|
+
puts q.query.sql
|
100
|
+
STDIN.gets
|
101
|
+
results = q.execute
|
102
|
+
if options[:full]
|
103
|
+
pp results
|
104
|
+
else
|
105
|
+
pp filtered(results)
|
106
|
+
end
|
107
|
+
puts results.length
|
108
|
+
end
|
109
|
+
|
110
|
+
def graph_it(statement, db = nil, title = nil)
|
111
|
+
require_relative 'graph'
|
112
|
+
require_relative 'tree'
|
113
|
+
ConceptQL::Graph.new(statement,
|
114
|
+
dangler: true,
|
115
|
+
title: title,
|
116
|
+
db: db
|
117
|
+
).graph_it('/tmp/graph')
|
118
|
+
system('open /tmp/graph.pdf')
|
119
|
+
end
|
120
|
+
|
121
|
+
def criteria_from_file(file)
|
122
|
+
case File.extname(file)
|
123
|
+
when '.json'
|
124
|
+
JSON.parse(File.read(file))
|
125
|
+
else
|
126
|
+
eval(File.read(file))
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def filtered(results)
|
131
|
+
results.each { |r| r.delete_if { |k,v| v.nil? } }
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require_relative '../conceptql'
|
2
|
+
|
3
|
+
module ConceptQL
|
4
|
+
# Used to translate a string of terse date adjustments into a set of adjustments that are compatible with most RDBMSs
|
5
|
+
class DateAdjuster
|
6
|
+
attr :str
|
7
|
+
def initialize(str)
|
8
|
+
@str = str
|
9
|
+
end
|
10
|
+
|
11
|
+
# Returns an array of strings that represent date modifiers
|
12
|
+
def adjustments
|
13
|
+
@adjustments ||= parse(str)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
def lookup
|
18
|
+
{
|
19
|
+
'y' => 'year',
|
20
|
+
'm' => 'month',
|
21
|
+
'd' => 'day'
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
def parse(str)
|
26
|
+
ConceptQL.logger.debug(str)
|
27
|
+
return [] if str.nil?
|
28
|
+
return ["#{str} days"] if str.match(/^[-+]?\d+$/)
|
29
|
+
str.downcase.scan(/([-+]?\d*[dmy])/).map do |adjustment|
|
30
|
+
adjustment = adjustment.first
|
31
|
+
quantity = 1
|
32
|
+
if adjustment.match(/\d/)
|
33
|
+
quantity = adjustment.to_i
|
34
|
+
else
|
35
|
+
if adjustment.chars.first == '-'
|
36
|
+
quantity = -1
|
37
|
+
end
|
38
|
+
end
|
39
|
+
unit = lookup[adjustment.chars.last]
|
40
|
+
unit += 's' if quantity.abs > 1
|
41
|
+
[quantity, unit].join(' ')
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'psych'
|
2
|
+
require 'graphviz'
|
3
|
+
require_relative 'tree'
|
4
|
+
require_relative 'nodes/node'
|
5
|
+
require_relative 'behaviors/dottable'
|
6
|
+
|
7
|
+
module ConceptQL
|
8
|
+
class Graph
|
9
|
+
attr :g, :file_path, :dangler, :title, :statement, :db, :suffix
|
10
|
+
def initialize(statement, opts = {})
|
11
|
+
@statement = statement
|
12
|
+
@db = opts.fetch(:db, nil)
|
13
|
+
@dangler = opts.fetch(:dangler, false)
|
14
|
+
@tree = opts.fetch(:tree, Tree.new)
|
15
|
+
@title = opts.fetch(:title, nil)
|
16
|
+
@suffix = opts.fetch(:suffix, 'pdf')
|
17
|
+
ConceptQL::Nodes::Node.send(:include, ConceptQL::Behaviors::Dottable)
|
18
|
+
end
|
19
|
+
|
20
|
+
def graph_it(file_path)
|
21
|
+
build_graph(g)
|
22
|
+
g.output(suffix.to_sym => file_path + ".#{suffix}")
|
23
|
+
end
|
24
|
+
|
25
|
+
def g
|
26
|
+
@g ||= begin
|
27
|
+
opts = { type: :digraph }
|
28
|
+
opts[:label] = title if title
|
29
|
+
GraphViz.new(:G, opts)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
attr :yaml, :tree, :db
|
35
|
+
|
36
|
+
def build_graph(g)
|
37
|
+
last_node = tree.root(self)
|
38
|
+
last_node.graph_it(g, db)
|
39
|
+
if dangler
|
40
|
+
blank_node = g.add_nodes('')
|
41
|
+
blank_node[:shape] = 'none'
|
42
|
+
blank_node[:height] = 0
|
43
|
+
blank_node[:fixedsize] = true
|
44
|
+
last_node.link_to(g, blank_node)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
@@ -0,0 +1,123 @@
|
|
1
|
+
require_relative 'behaviors/dottable'
|
2
|
+
require_relative 'nodes/node'
|
3
|
+
|
4
|
+
module ConceptQL
|
5
|
+
class GraphNodifier
|
6
|
+
class DotNode < ConceptQL::Nodes::Node
|
7
|
+
include ConceptQL::Behaviors::Dottable
|
8
|
+
|
9
|
+
TYPES = {
|
10
|
+
# Conditions
|
11
|
+
condition: :condition_occurrence,
|
12
|
+
primary_diagnosis: :condition_occurrence,
|
13
|
+
icd9: :condition_occurrence,
|
14
|
+
|
15
|
+
# Procedures
|
16
|
+
procedure: :procedure_occurrence,
|
17
|
+
cpt: :procedure_occurrence,
|
18
|
+
hcpcs: :procedure_occurrence,
|
19
|
+
icd9_procedure: :procedure_occurrence,
|
20
|
+
procedure_cost: :procedure_cost,
|
21
|
+
|
22
|
+
# Visits
|
23
|
+
visit_occurrence: :visit_occurrence,
|
24
|
+
place_of_service: :visit_occurrence,
|
25
|
+
|
26
|
+
# Person
|
27
|
+
person: :person,
|
28
|
+
gender: :person,
|
29
|
+
race: :person,
|
30
|
+
|
31
|
+
# Payer
|
32
|
+
payer: :payer_plan_period,
|
33
|
+
|
34
|
+
# Death
|
35
|
+
death: :death,
|
36
|
+
|
37
|
+
# Observation
|
38
|
+
loinc: :observation,
|
39
|
+
|
40
|
+
# Drug
|
41
|
+
drug_exposure: :drug_exposure,
|
42
|
+
drug_cost: :drug_cost,
|
43
|
+
|
44
|
+
# Date Nodes
|
45
|
+
date_range: :date,
|
46
|
+
|
47
|
+
# Miscelaneous nodes
|
48
|
+
concept: :misc
|
49
|
+
}
|
50
|
+
|
51
|
+
attr :values, :name
|
52
|
+
def initialize(name, values)
|
53
|
+
@name = name.to_s
|
54
|
+
super(values)
|
55
|
+
end
|
56
|
+
|
57
|
+
def display_name
|
58
|
+
@__display_name ||= begin
|
59
|
+
output = @name.dup
|
60
|
+
output += ": #{arguments.join(', ')}" unless arguments.empty?
|
61
|
+
if output.length > 100
|
62
|
+
parts = output.split
|
63
|
+
output = parts.each_slice(output.length / parts.count).map do |subparts|
|
64
|
+
subparts.join(' ')
|
65
|
+
end.join ('\n')
|
66
|
+
end
|
67
|
+
output += "\n#{options.map{|k,v| "#{k}: #{v}"}.join("\n")}" unless options.nil? || options.empty?
|
68
|
+
output
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def types
|
73
|
+
types = [TYPES[name.to_sym] || children.map(&:types)].flatten.uniq
|
74
|
+
types.empty? ? [:misc] : types
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
class BinaryOperatorNode < DotNode
|
79
|
+
def display_name
|
80
|
+
output = name
|
81
|
+
output += "\n#{options.map{|k,v| "#{k}: #{v}"}.join("\n")}" unless options.nil? || options.empty?
|
82
|
+
output
|
83
|
+
end
|
84
|
+
|
85
|
+
def left
|
86
|
+
options[:left]
|
87
|
+
end
|
88
|
+
|
89
|
+
def right
|
90
|
+
options[:right]
|
91
|
+
end
|
92
|
+
|
93
|
+
def graph_it(g, db)
|
94
|
+
left.graph_it(g, db)
|
95
|
+
right.graph_it(g, db)
|
96
|
+
cluster_name = "cluster_#{node_name}"
|
97
|
+
me = g.send(cluster_name) do |sub|
|
98
|
+
sub[rank: 'same', label: display_name, color: 'black']
|
99
|
+
sub.send("#{cluster_name}_left").send('[]', shape: 'point', color: type_color(types))
|
100
|
+
sub.send("#{cluster_name}_right").send('[]', shape: 'point')
|
101
|
+
end
|
102
|
+
left.link_to(g, me.send("#{cluster_name}_left"))
|
103
|
+
right.link_to(g, me.send("#{cluster_name}_right"))
|
104
|
+
@__graph_node = me.send("#{cluster_name}_left")
|
105
|
+
end
|
106
|
+
|
107
|
+
def types
|
108
|
+
left.types
|
109
|
+
end
|
110
|
+
|
111
|
+
def arguments
|
112
|
+
options.values
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
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)
|
117
|
+
|
118
|
+
def create(type, values)
|
119
|
+
return BinaryOperatorNode.new(type, values) if BINARY_OPERATOR_TYPES.include?(type)
|
120
|
+
DotNode.new(type, values)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require_relative 'node'
|
2
|
+
|
3
|
+
module ConceptQL
|
4
|
+
module Nodes
|
5
|
+
# Base class for all nodes that take two streams, a left-hand and a right-hand
|
6
|
+
class BinaryOperatorNode < Node
|
7
|
+
def children
|
8
|
+
[left]
|
9
|
+
end
|
10
|
+
|
11
|
+
def graph_it(g, db)
|
12
|
+
left.graph_it(g, db)
|
13
|
+
right.graph_it(g, db)
|
14
|
+
cluster_name = "cluster_#{node_name}"
|
15
|
+
me = g.send(cluster_name) do |sub|
|
16
|
+
sub[rank: 'same', label: display_name, color: 'black']
|
17
|
+
sub.send("#{cluster_name}_left").send('[]', shape: 'point', color: type_color(types))
|
18
|
+
sub.send("#{cluster_name}_right").send('[]', shape: 'point')
|
19
|
+
end
|
20
|
+
left.link_to(g, me.send("#{cluster_name}_left"))
|
21
|
+
right.link_to(g, me.send("#{cluster_name}_right"))
|
22
|
+
@__graph_node = me.send("#{cluster_name}_left")
|
23
|
+
end
|
24
|
+
|
25
|
+
def display_name
|
26
|
+
self.class.name.split('::').last.titleize
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
def left
|
31
|
+
@left ||= options[:left]
|
32
|
+
end
|
33
|
+
|
34
|
+
def right
|
35
|
+
@right ||= options[:right]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require_relative 'node'
|
2
|
+
|
3
|
+
module ConceptQL
|
4
|
+
module Nodes
|
5
|
+
# Parent class of all casting nodes
|
6
|
+
#
|
7
|
+
# Subclasses must implement the following methods:
|
8
|
+
# - my_type
|
9
|
+
# - i_point_at
|
10
|
+
# - these_point_at_me
|
11
|
+
#
|
12
|
+
# i_point_at returns a list of types for which the node's table of origin
|
13
|
+
# has foreign_keys pointing to another table, e.g.:
|
14
|
+
# procedure_occurrence has an FK to visit_occurrence, so we'd put
|
15
|
+
# :visit_occurrence in the i_point_at array
|
16
|
+
#
|
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 node's
|
19
|
+
# table of origin, e.g.:
|
20
|
+
# procedure_cost has an FK to procedure_occurrence so we'd
|
21
|
+
# put :procedure_cost in procedure_occurrence's these_point_at_me array
|
22
|
+
#
|
23
|
+
# Also, if a casting node is passed no streams, it will return all the
|
24
|
+
# rows in its table as results.
|
25
|
+
class CastingNode < Node
|
26
|
+
def types
|
27
|
+
[my_type]
|
28
|
+
end
|
29
|
+
|
30
|
+
def castables
|
31
|
+
(i_point_at + these_point_at_me)
|
32
|
+
end
|
33
|
+
|
34
|
+
def query(db)
|
35
|
+
return db.from(make_table_name(my_type)) if stream.nil?
|
36
|
+
base_query(db, stream.evaluate(db))
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def base_query(db, stream_query)
|
42
|
+
if (stream.types & castables).length < stream.types.length
|
43
|
+
# We have a situation where one of the incoming streams
|
44
|
+
# isn't castable so we'll just return all rows for
|
45
|
+
# all people
|
46
|
+
db.from(make_table_name(my_type))
|
47
|
+
.where(person_id: db.from(stream_query).select_group(:person_id))
|
48
|
+
else
|
49
|
+
# Every type in the stream is castable, so let's setup a query that
|
50
|
+
# casts each type to a set of IDs, union those IDs and fetch
|
51
|
+
# them from the source table
|
52
|
+
my_ids = stream.types.map do |type|
|
53
|
+
cast_type(db, type, stream_query)
|
54
|
+
end.inject do |union_query, query|
|
55
|
+
union_query.union(query)
|
56
|
+
end
|
57
|
+
|
58
|
+
db.from(make_table_name(my_type))
|
59
|
+
.where(type_id(my_type) => my_ids)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def cast_type(db, type, stream_query)
|
64
|
+
query = if i_point_at.include?(type)
|
65
|
+
db.from(make_table_name(my_type))
|
66
|
+
.where(type_id(type) => db.from(stream_query.select_group(type_id(type))))
|
67
|
+
else
|
68
|
+
db.from(make_table_name(type))
|
69
|
+
.where(type_id(type) => db.from(stream_query.select_group(type_id(type))))
|
70
|
+
end
|
71
|
+
query.select(type_id(my_type))
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require_relative 'pass_thru'
|
2
|
+
|
3
|
+
module ConceptQL
|
4
|
+
module Nodes
|
5
|
+
class Complement < PassThru
|
6
|
+
def query(db)
|
7
|
+
child = children.first
|
8
|
+
child.types.map do |type|
|
9
|
+
select_it(db.from(make_table_name(type)).exclude(type_id(type) => child.evaluate(db).select(type_id(type)).from_self.exclude(type_id(type) => nil)), [type])
|
10
|
+
end.inject do |union_query, q|
|
11
|
+
union_query.union(q, all: true)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require_relative 'node'
|
2
|
+
require_relative '../tree'
|
3
|
+
|
4
|
+
module ConceptQL
|
5
|
+
module Nodes
|
6
|
+
class Concept < Node
|
7
|
+
attr :statement
|
8
|
+
def query(db)
|
9
|
+
set_statement(db)
|
10
|
+
stream.evaluate(db)
|
11
|
+
end
|
12
|
+
|
13
|
+
def children
|
14
|
+
@children ||= [ Tree.new.root(self) ]
|
15
|
+
end
|
16
|
+
|
17
|
+
def graph_prep(db)
|
18
|
+
set_statement(db)
|
19
|
+
@arguments = [description(db)]
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
def set_statement(db)
|
24
|
+
@statement ||= db[:concepts].where(concept_id: arguments.first).select_map(:statement).first.tap { |f| ConceptQL.logger.debug f.inspect }.to_hash
|
25
|
+
end
|
26
|
+
|
27
|
+
def description(db)
|
28
|
+
@description ||= db[:concepts].where(concept_id: arguments.first).select_map(:label).first
|
29
|
+
end
|
30
|
+
|
31
|
+
def db
|
32
|
+
tree.db
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require_relative 'node'
|
2
|
+
|
3
|
+
module ConceptQL
|
4
|
+
module Nodes
|
5
|
+
# Represents a node that will grab all conditions that match the
|
6
|
+
# condition type passed in
|
7
|
+
#
|
8
|
+
# Condition Type represents which position the condition held in
|
9
|
+
# the raw data, e.g. primary inpatient header or 15th outpatient detail
|
10
|
+
#
|
11
|
+
# Multiple types can be specified at once
|
12
|
+
class ConditionType < Node
|
13
|
+
def types
|
14
|
+
[:condition_occurrence]
|
15
|
+
end
|
16
|
+
|
17
|
+
def query(db)
|
18
|
+
db.from(:condition_occurrence_with_dates)
|
19
|
+
.where(condition_type_concept_id: condition_occurrence_type_concept_ids)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
def condition_occurrence_type_concept_ids
|
24
|
+
arguments.map do |arg|
|
25
|
+
to_concept_id(arg.to_s)
|
26
|
+
end.flatten
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_concept_id(ctype)
|
30
|
+
ctype = ctype.to_s.downcase
|
31
|
+
position = nil
|
32
|
+
if ctype =~ /(\d|_primary)$/
|
33
|
+
parts = ctype.split('_')
|
34
|
+
position = parts.pop.to_i
|
35
|
+
position -= 1 if ctype =~ /^outpatient/
|
36
|
+
ctype = parts.join('_')
|
37
|
+
end
|
38
|
+
retval = concept_ids[ctype.to_sym]
|
39
|
+
return retval[position] if position
|
40
|
+
return retval
|
41
|
+
end
|
42
|
+
|
43
|
+
def concept_ids
|
44
|
+
@concept_ids ||= begin
|
45
|
+
hash = {
|
46
|
+
inpatient_detail: (38000183..38000198).to_a,
|
47
|
+
inpatient_header: (38000199..38000214).to_a,
|
48
|
+
outpatient_detail: (38000215..38000229).to_a,
|
49
|
+
outpatient_header: (38000230..38000244).to_a,
|
50
|
+
ehr_problem_list: [38000245],
|
51
|
+
condition_era_0_day_window: [38000246],
|
52
|
+
condition_era_30_day_window: [38000247]
|
53
|
+
}
|
54
|
+
hash[:inpatient] = hash[:inpatient_detail] + hash[:inpatient_header]
|
55
|
+
hash[:outpatient] = hash[:outpatient_detail] + hash[:outpatient_header]
|
56
|
+
hash[:condition_era] = hash[:condition_era_0_day_window] + hash[:condition_era_30_day_window]
|
57
|
+
hash
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|