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.
Files changed (111) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/CHANGELOG.md +17 -0
  4. data/Gemfile +4 -0
  5. data/Guardfile +28 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +108 -0
  8. data/Rakefile +1 -0
  9. data/bin/conceptql +5 -0
  10. data/conceptql.gemspec +30 -0
  11. data/doc/ConceptQL Specification (alpha).pdf +0 -0
  12. data/doc/diagram_0.png +0 -0
  13. data/doc/spec.md +1208 -0
  14. data/lib/conceptql/behaviors/dottable.rb +71 -0
  15. data/lib/conceptql/cli.rb +135 -0
  16. data/lib/conceptql/date_adjuster.rb +45 -0
  17. data/lib/conceptql/graph.rb +49 -0
  18. data/lib/conceptql/graph_nodifier.rb +123 -0
  19. data/lib/conceptql/logger.rb +10 -0
  20. data/lib/conceptql/nodes/after.rb +12 -0
  21. data/lib/conceptql/nodes/before.rb +11 -0
  22. data/lib/conceptql/nodes/binary_operator_node.rb +41 -0
  23. data/lib/conceptql/nodes/casting_node.rb +75 -0
  24. data/lib/conceptql/nodes/complement.rb +16 -0
  25. data/lib/conceptql/nodes/concept.rb +38 -0
  26. data/lib/conceptql/nodes/condition_type.rb +63 -0
  27. data/lib/conceptql/nodes/cpt.rb +20 -0
  28. data/lib/conceptql/nodes/date_range.rb +39 -0
  29. data/lib/conceptql/nodes/death.rb +19 -0
  30. data/lib/conceptql/nodes/during.rb +16 -0
  31. data/lib/conceptql/nodes/except.rb +11 -0
  32. data/lib/conceptql/nodes/first.rb +24 -0
  33. data/lib/conceptql/nodes/from.rb +15 -0
  34. data/lib/conceptql/nodes/gender.rb +27 -0
  35. data/lib/conceptql/nodes/hcpcs.rb +20 -0
  36. data/lib/conceptql/nodes/icd10.rb +23 -0
  37. data/lib/conceptql/nodes/icd9.rb +23 -0
  38. data/lib/conceptql/nodes/icd9_procedure.rb +20 -0
  39. data/lib/conceptql/nodes/intersect.rb +29 -0
  40. data/lib/conceptql/nodes/last.rb +24 -0
  41. data/lib/conceptql/nodes/loinc.rb +20 -0
  42. data/lib/conceptql/nodes/node.rb +71 -0
  43. data/lib/conceptql/nodes/occurrence.rb +47 -0
  44. data/lib/conceptql/nodes/pass_thru.rb +11 -0
  45. data/lib/conceptql/nodes/person.rb +25 -0
  46. data/lib/conceptql/nodes/person_filter.rb +12 -0
  47. data/lib/conceptql/nodes/place_of_service_code.rb +23 -0
  48. data/lib/conceptql/nodes/procedure_occurrence.rb +21 -0
  49. data/lib/conceptql/nodes/race.rb +23 -0
  50. data/lib/conceptql/nodes/rxnorm.rb +20 -0
  51. data/lib/conceptql/nodes/snomed.rb +19 -0
  52. data/lib/conceptql/nodes/source_vocabulary_node.rb +54 -0
  53. data/lib/conceptql/nodes/standard_vocabulary_node.rb +43 -0
  54. data/lib/conceptql/nodes/started_by.rb +16 -0
  55. data/lib/conceptql/nodes/temporal_node.rb +25 -0
  56. data/lib/conceptql/nodes/time_window.rb +54 -0
  57. data/lib/conceptql/nodes/union.rb +15 -0
  58. data/lib/conceptql/nodes/visit.rb +11 -0
  59. data/lib/conceptql/nodes/visit_occurrence.rb +26 -0
  60. data/lib/conceptql/nodifier.rb +9 -0
  61. data/lib/conceptql/query.rb +39 -0
  62. data/lib/conceptql/tree.rb +36 -0
  63. data/lib/conceptql/version.rb +3 -0
  64. data/lib/conceptql/view_maker.rb +56 -0
  65. data/lib/conceptql.rb +7 -0
  66. data/spec/conceptql/behaviors/dottable_spec.rb +111 -0
  67. data/spec/conceptql/date_adjuster_spec.rb +68 -0
  68. data/spec/conceptql/nodes/after_spec.rb +18 -0
  69. data/spec/conceptql/nodes/before_spec.rb +18 -0
  70. data/spec/conceptql/nodes/casting_node_spec.rb +73 -0
  71. data/spec/conceptql/nodes/complement_spec.rb +15 -0
  72. data/spec/conceptql/nodes/concept_spec.rb +34 -0
  73. data/spec/conceptql/nodes/condition_type_spec.rb +113 -0
  74. data/spec/conceptql/nodes/cpt_spec.rb +31 -0
  75. data/spec/conceptql/nodes/date_range_spec.rb +35 -0
  76. data/spec/conceptql/nodes/death_spec.rb +12 -0
  77. data/spec/conceptql/nodes/during_spec.rb +32 -0
  78. data/spec/conceptql/nodes/except_spec.rb +18 -0
  79. data/spec/conceptql/nodes/first_spec.rb +37 -0
  80. data/spec/conceptql/nodes/from_spec.rb +15 -0
  81. data/spec/conceptql/nodes/gender_spec.rb +29 -0
  82. data/spec/conceptql/nodes/hcpcs_spec.rb +31 -0
  83. data/spec/conceptql/nodes/icd10_spec.rb +36 -0
  84. data/spec/conceptql/nodes/icd9_procedure_spec.rb +31 -0
  85. data/spec/conceptql/nodes/icd9_spec.rb +36 -0
  86. data/spec/conceptql/nodes/intersect_spec.rb +33 -0
  87. data/spec/conceptql/nodes/last_spec.rb +38 -0
  88. data/spec/conceptql/nodes/loinc_spec.rb +31 -0
  89. data/spec/conceptql/nodes/occurrence_spec.rb +89 -0
  90. data/spec/conceptql/nodes/person_filter_spec.rb +18 -0
  91. data/spec/conceptql/nodes/person_spec.rb +12 -0
  92. data/spec/conceptql/nodes/place_of_service_code_spec.rb +26 -0
  93. data/spec/conceptql/nodes/procedure_occurrence_spec.rb +12 -0
  94. data/spec/conceptql/nodes/query_double.rb +19 -0
  95. data/spec/conceptql/nodes/race_spec.rb +23 -0
  96. data/spec/conceptql/nodes/rxnorm_spec.rb +31 -0
  97. data/spec/conceptql/nodes/snomed_spec.rb +31 -0
  98. data/spec/conceptql/nodes/source_vocabulary_node_spec.rb +37 -0
  99. data/spec/conceptql/nodes/standard_vocabulary_node_spec.rb +40 -0
  100. data/spec/conceptql/nodes/started_by_spec.rb +25 -0
  101. data/spec/conceptql/nodes/temporal_node_spec.rb +57 -0
  102. data/spec/conceptql/nodes/time_window_spec.rb +66 -0
  103. data/spec/conceptql/nodes/union_spec.rb +25 -0
  104. data/spec/conceptql/nodes/visit_occurrence_spec.rb +12 -0
  105. data/spec/conceptql/query_spec.rb +20 -0
  106. data/spec/conceptql/tree_spec.rb +54 -0
  107. data/spec/doubles/stream_for_casting_double.rb +9 -0
  108. data/spec/doubles/stream_for_occurrence_double.rb +21 -0
  109. data/spec/doubles/stream_for_temporal_double.rb +6 -0
  110. data/spec/spec_helper.rb +74 -0
  111. 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,10 @@
1
+ require 'logger'
2
+ module ConceptQL
3
+ def self.logger
4
+ @logger ||= begin
5
+ l = Logger.new('/tmp/cql.log')
6
+ l.level = Logger::DEBUG
7
+ l
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,12 @@
1
+ require_relative 'temporal_node'
2
+
3
+ module ConceptQL
4
+ module Nodes
5
+ class After < TemporalNode
6
+ def where_clause
7
+ Proc.new { l__start_date > r__end_date }
8
+ end
9
+ end
10
+ end
11
+ end
12
+
@@ -0,0 +1,11 @@
1
+ require_relative 'temporal_node'
2
+
3
+ module ConceptQL
4
+ module Nodes
5
+ class Before < TemporalNode
6
+ def where_clause
7
+ Proc.new { l__end_date < r__start_date }
8
+ end
9
+ end
10
+ end
11
+ 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
+