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,20 @@
1
+ require_relative 'standard_vocabulary_node'
2
+
3
+ module ConceptQL
4
+ module Nodes
5
+ class Cpt < StandardVocabularyNode
6
+ def table
7
+ :procedure_occurrence
8
+ end
9
+
10
+ def vocabulary_id
11
+ 4
12
+ end
13
+
14
+ def concept_column
15
+ :procedure_concept_id
16
+ end
17
+ end
18
+ end
19
+ end
20
+
@@ -0,0 +1,39 @@
1
+ require_relative 'node'
2
+
3
+ module ConceptQL
4
+ module Nodes
5
+ # Represents a node that will create a date_range for every person in the database
6
+ #
7
+ # Accepts two params: start and end formateed as 'YYYY-MM-DD' or 'START' or 'END'
8
+ # 'START' represents the first date of data in the data source,
9
+ # 'END' represents the last date of data in the data source,
10
+ class DateRange < Node
11
+ def query(db)
12
+ db.from(:person)
13
+ .select_append(Sequel.expr(start_date(db)).cast(:date).as(:start_date),Sequel.expr(end_date(db)).cast(:date).as(:end_date)).from_self
14
+ end
15
+
16
+ def types
17
+ [:person]
18
+ end
19
+
20
+ private
21
+ def start_date(db)
22
+ date_from(db, options[:start])
23
+ end
24
+
25
+ def end_date(db)
26
+ date_from(db, options[:end])
27
+ end
28
+
29
+ # TODO: Select the earliest and latest dates of observation from
30
+ # the proper CDM table to represent the start and end of data
31
+ def date_from(db, str)
32
+ return db.from(:visit_occurrence_with_dates).select { min(:start_date) } if str.upcase == 'START'
33
+ return db.from(:visit_occurrence_with_dates).select { max(:end_date) } if str.upcase == 'END'
34
+ return str
35
+ end
36
+ end
37
+ end
38
+ end
39
+
@@ -0,0 +1,19 @@
1
+ require_relative 'casting_node'
2
+
3
+ module ConceptQL
4
+ module Nodes
5
+ class Death < CastingNode
6
+ def my_type
7
+ :death
8
+ end
9
+
10
+ def i_point_at
11
+ [ :person ]
12
+ end
13
+
14
+ def these_point_at_me
15
+ []
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,16 @@
1
+ require_relative 'temporal_node'
2
+
3
+ module ConceptQL
4
+ module Nodes
5
+ class During < TemporalNode
6
+ def where_clause
7
+ if inclusive?
8
+ Sequel.expr(Sequel.expr(Proc.new { r__start_date <= l__start_date}).&(Sequel.expr( Proc.new { l__start_date <= r__end_date })))
9
+ .|(Sequel.expr(Proc.new { r__start_date <= l__end_date}).&(Sequel.expr( Proc.new { l__end_date <= r__end_date })))
10
+ else
11
+ [Proc.new { r__start_date <= l__start_date}, Proc.new { l__end_date <= r__end_date }]
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,11 @@
1
+ require_relative 'binary_operator_node'
2
+
3
+ module ConceptQL
4
+ module Nodes
5
+ class Except < BinaryOperatorNode
6
+ def query(db)
7
+ left.evaluate(db).except(right.evaluate(db))
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,24 @@
1
+ require_relative 'occurrence'
2
+
3
+ module ConceptQL
4
+ module Nodes
5
+ # Represents a node that will grab the first occurrence of something
6
+ #
7
+ # The node treats all streams as a single, large stream. It partitions
8
+ # that larget stream by person_id, then sorts within those groupings
9
+ # by start_date and then select at most one row per person, regardless
10
+ # of how many different types of streams enter the node
11
+ #
12
+ # If two rows have the same start_date, the order of their ranking
13
+ # is arbitrary
14
+ #
15
+ # If we ask for the first occurrence of something and a person has no
16
+ # occurrences, this node returns nothing for that person
17
+ class First < Occurrence
18
+ def occurrence
19
+ 1
20
+ end
21
+ end
22
+ end
23
+ end
24
+
@@ -0,0 +1,15 @@
1
+ require_relative 'pass_thru'
2
+
3
+ module ConceptQL
4
+ module Nodes
5
+ class From < Node
6
+ def query(db)
7
+ db.from(values.first)
8
+ end
9
+
10
+ def types
11
+ values[1..99].compact
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,27 @@
1
+ require_relative 'node'
2
+
3
+ module ConceptQL
4
+ module Nodes
5
+ class Gender < Node
6
+ def types
7
+ [:person]
8
+ end
9
+
10
+ def query(db)
11
+ gender_concept_ids = values.map do |value|
12
+ case value.to_s
13
+ when /^m/i
14
+ 8507
15
+ when /^f/i
16
+ 8532
17
+ else
18
+ value.to_i
19
+ end
20
+ end
21
+
22
+ db.from(:person_with_dates)
23
+ .where(gender_concept_id: gender_concept_ids)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,20 @@
1
+ require_relative 'standard_vocabulary_node'
2
+
3
+ module ConceptQL
4
+ module Nodes
5
+ class Hcpcs < StandardVocabularyNode
6
+ def table
7
+ :procedure_occurrence
8
+ end
9
+
10
+ def vocabulary_id
11
+ 5
12
+ end
13
+
14
+ def concept_column
15
+ :procedure_concept_id
16
+ end
17
+ end
18
+ end
19
+ end
20
+
@@ -0,0 +1,23 @@
1
+ require_relative 'source_vocabulary_node'
2
+
3
+ module ConceptQL
4
+ module Nodes
5
+ class Icd10 < SourceVocabularyNode
6
+ def table
7
+ :condition_occurrence
8
+ end
9
+
10
+ def vocabulary_id
11
+ 34
12
+ end
13
+
14
+ def source_column
15
+ :condition_source_value
16
+ end
17
+
18
+ def concept_column
19
+ :condition_concept_id
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ require_relative 'source_vocabulary_node'
2
+
3
+ module ConceptQL
4
+ module Nodes
5
+ class Icd9 < SourceVocabularyNode
6
+ def table
7
+ :condition_occurrence
8
+ end
9
+
10
+ def vocabulary_id
11
+ 2
12
+ end
13
+
14
+ def source_column
15
+ :condition_source_value
16
+ end
17
+
18
+ def concept_column
19
+ :condition_concept_id
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,20 @@
1
+ require_relative 'standard_vocabulary_node'
2
+
3
+ module ConceptQL
4
+ module Nodes
5
+ class Icd9Procedure < StandardVocabularyNode
6
+ def table
7
+ :procedure_occurrence
8
+ end
9
+
10
+ def vocabulary_id
11
+ 3
12
+ end
13
+
14
+ def concept_column
15
+ :procedure_concept_id
16
+ end
17
+ end
18
+ end
19
+ end
20
+
@@ -0,0 +1,29 @@
1
+ require_relative 'pass_thru'
2
+
3
+ module ConceptQL
4
+ module Nodes
5
+ class Intersect < PassThru
6
+ def types
7
+ values.map(&:types).flatten.uniq
8
+ end
9
+
10
+ def query(db)
11
+ exprs = {}
12
+ values.each do |expression|
13
+ expression.types.each do |type|
14
+ (exprs[type] ||= []) << expression.evaluate(db)
15
+ end
16
+ end
17
+ typed_queries = exprs.map do |type, queries|
18
+ queries.inject do |q, query|
19
+ q.intersect(query, all: true)
20
+ end
21
+ end
22
+
23
+ typed_queries.inject do |q, query|
24
+ q.union(query, all: true)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,24 @@
1
+ require_relative 'occurrence'
2
+
3
+ module ConceptQL
4
+ module Nodes
5
+ # Represents a node that will grab the last occurrence of something
6
+ #
7
+ # The node treats all streams as a single, large stream. It partitions
8
+ # that larget stream by person_id, then sorts within those groupings
9
+ # by start_date and then select at most one row per person, regardless
10
+ # of how many different types of streams enter the node
11
+ #
12
+ # If two rows have the same start_date, the order of their ranking
13
+ # is arbitrary
14
+ #
15
+ # If we ask for the last occurrence of something and a person has no
16
+ # occurrences, this node returns nothing for that person
17
+ class Last < Occurrence
18
+ def occurrence
19
+ -1
20
+ end
21
+ end
22
+ end
23
+ end
24
+
@@ -0,0 +1,20 @@
1
+ require_relative 'standard_vocabulary_node'
2
+
3
+ module ConceptQL
4
+ module Nodes
5
+ class Loinc < StandardVocabularyNode
6
+ def table
7
+ :observation
8
+ end
9
+
10
+ def vocabulary_id
11
+ 6
12
+ end
13
+
14
+ def concept_column
15
+ :observation_concept_id
16
+ end
17
+ end
18
+ end
19
+ end
20
+
@@ -0,0 +1,71 @@
1
+ require 'active_support/core_ext/hash'
2
+ module ConceptQL
3
+ module Nodes
4
+ class Node
5
+ KNOWN_TYPES = %i(
6
+ condition_occurrence
7
+ death
8
+ drug_cost
9
+ drug_exposure
10
+ observation
11
+ payer_plan_period
12
+ procedure_cost
13
+ procedure_occurrence
14
+ visit_occurrence
15
+ )
16
+
17
+ attr :values, :options
18
+ def initialize(*args)
19
+ args.flatten!
20
+ if args.last.is_a?(Hash)
21
+ @options = args.pop.symbolize_keys
22
+ end
23
+ @options ||= {}
24
+ @values = args.flatten
25
+ end
26
+
27
+ def evaluate(db)
28
+ select_it(query(db))
29
+ end
30
+
31
+ def select_it(query, select_types=types)
32
+ query.select(*columns(select_types))
33
+ end
34
+
35
+ def types
36
+ @types ||= children.map(&:types).flatten.uniq
37
+ end
38
+
39
+ def children
40
+ @children ||= values.select { |v| v.is_a?(Node) }
41
+ end
42
+
43
+ def stream
44
+ @stream ||= children.first
45
+ end
46
+
47
+ def arguments
48
+ @arguments ||= values.reject { |v| v.is_a?(Node) }
49
+ end
50
+
51
+ def columns(select_types = types)
52
+ [:person_id___person_id] + KNOWN_TYPES.map do |known_type|
53
+ select_types.include?(known_type) ? "#{known_type}_id___#{known_type}_id".to_sym : Sequel.expr(nil).cast(:bigint).as("#{known_type}_id".to_sym)
54
+ end + date_columns
55
+ end
56
+
57
+ def date_columns
58
+ [:start_date, :end_date]
59
+ end
60
+
61
+ private
62
+ def type_id(type)
63
+ (type.to_s + '_id').to_sym
64
+ end
65
+
66
+ def make_table_name(table)
67
+ "#{table}_with_dates___tab".to_sym
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,47 @@
1
+ require_relative 'node'
2
+
3
+ module ConceptQL
4
+ module Nodes
5
+ # Represents a node that will grab the Nth occurrence of something
6
+ #
7
+ # Specify occurrences as integers, excluding O
8
+ # 1 => first
9
+ # 2 => second
10
+ # ...
11
+ # -1 => last
12
+ # -2 => second-to-last
13
+ #
14
+ # The node treats all streams as a single, large stream. It partitions
15
+ # that larget stream by person_id, then sorts within those groupings
16
+ # by start_date and then select at most one row per person, regardless
17
+ # of how many different types of streams enter the node
18
+ #
19
+ # If two rows have the same start_date, the order of their ranking
20
+ # is arbitrary
21
+ #
22
+ # If we ask for the second occurrence of something and a person has only one
23
+ # occurrence, this node returns nothing for that person
24
+ class Occurrence < Node
25
+ def query(db)
26
+ db.from(stream.evaluate(db)
27
+ .select_append { |o| o.row_number(:over, partition: :person_id, order: ordered_columns){}.as(:rn) })
28
+ .where(rn: occurrence.abs)
29
+ end
30
+
31
+ def occurrence
32
+ @occurrence ||= arguments.first
33
+ end
34
+
35
+ private
36
+ def asc_or_desc
37
+ occurrence < 0 ? :desc : :asc
38
+ end
39
+
40
+ def ordered_columns
41
+ ordered_columns = [Sequel.send(asc_or_desc, :start_date)]
42
+ ordered_columns += types.map { |t| Sequel.asc(type_id(t)) }
43
+ end
44
+ end
45
+ end
46
+ end
47
+
@@ -0,0 +1,11 @@
1
+ require_relative 'node'
2
+
3
+ module ConceptQL
4
+ module Nodes
5
+ class PassThru < Node
6
+ def types
7
+ values.map(&:types).flatten.uniq
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,25 @@
1
+ require_relative 'casting_node'
2
+
3
+ module ConceptQL
4
+ module Nodes
5
+ class Person < CastingNode
6
+ def my_type
7
+ :person
8
+ end
9
+
10
+ def i_point_at
11
+ []
12
+ end
13
+
14
+ def these_point_at_me
15
+ # I could list ALL the types we use, but the default behavior of casting,
16
+ # when there is no explicit casting defined, is to convert everything to
17
+ # person IDs
18
+ #
19
+ # So by defining no known castable relationships in this node, all
20
+ # types will be converted to person
21
+ []
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,12 @@
1
+ require_relative 'binary_operator_node'
2
+
3
+ module ConceptQL
4
+ module Nodes
5
+ class PersonFilter < BinaryOperatorNode
6
+ def query(db)
7
+ db.from(left.evaluate(db))
8
+ .where(person_id: right.evaluate(db).select_group(:person_id))
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,23 @@
1
+ require_relative 'node'
2
+
3
+ module ConceptQL
4
+ module Nodes
5
+ # Represents a node that will grab all person rows that match the given place_of_service_codes
6
+ #
7
+ # PlaceOfServiceCode parameters are passed in as a set of strings. Each string represents
8
+ # a single place_of_service_code. The place_of_service_code string must match one of the values in the
9
+ # concept_name column of the concept table. If you misspell the place_of_service_code name
10
+ # you won't get any matches
11
+ class PlaceOfServiceCode < Node
12
+ def types
13
+ [:visit_occurrence]
14
+ end
15
+
16
+ def query(db)
17
+ db.from(:visit_occurrence_with_dates___v)
18
+ .join(:vocabulary__concept___vc, { vc__concept_id: :v__place_of_service_concept_id })
19
+ .where(vc__concept_code: arguments.map(&:to_s))
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,21 @@
1
+ require_relative 'casting_node'
2
+
3
+ module ConceptQL
4
+ module Nodes
5
+ class ProcedureOccurrence < CastingNode
6
+ def my_type
7
+ :procedure_occurrence
8
+ end
9
+
10
+ def i_point_at
11
+ [ :person ]
12
+ end
13
+
14
+ def these_point_at_me
15
+ %i[
16
+ procedure_cost
17
+ ]
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,23 @@
1
+ require_relative 'node'
2
+
3
+ module ConceptQL
4
+ module Nodes
5
+ # Represents a node that will grab all person rows that match the given races
6
+ #
7
+ # Race parameters are passed in as a set of strings. Each string represents
8
+ # a single race. The race string must match one of the values in the
9
+ # concept_name column of the concept table. If you misspell the race name
10
+ # you won't get any matches
11
+ class Race < Node
12
+ def types
13
+ [:person]
14
+ end
15
+
16
+ def query(db)
17
+ db.from(:person_with_dates___p)
18
+ .join(:vocabulary__concept___vc, { vc__concept_id: :p__race_concept_id })
19
+ .where(Sequel.function(:lower, :vc__concept_name) => arguments.map(&:downcase))
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,20 @@
1
+ require_relative 'standard_vocabulary_node'
2
+
3
+ module ConceptQL
4
+ module Nodes
5
+ class Rxnorm < StandardVocabularyNode
6
+ def table
7
+ :drug_exposure
8
+ end
9
+
10
+ def vocabulary_id
11
+ 8
12
+ end
13
+
14
+ def concept_column
15
+ :drug_concept_id
16
+ end
17
+ end
18
+ end
19
+ end
20
+
@@ -0,0 +1,19 @@
1
+ require_relative 'standard_vocabulary_node'
2
+
3
+ module ConceptQL
4
+ module Nodes
5
+ class Snomed < StandardVocabularyNode
6
+ def table
7
+ :condition_occurrence
8
+ end
9
+
10
+ def vocabulary_id
11
+ 1
12
+ end
13
+
14
+ def concept_column
15
+ :condition_concept_id
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,54 @@
1
+ require_relative 'node'
2
+
3
+ module ConceptQL
4
+ module Nodes
5
+ # A SourceVocabularyNode is a superclass for a node that represents a criterion whose column stores information associated with a source vocabulary.
6
+ #
7
+ # If that seems confusing, then think of ICD-9 or NDC criteria. That type of criterion takes a set of values that are mapped via the source_to_concept_map table into a standard vocabulary.
8
+ #
9
+ # This kind of criterion can have some interesting issues when we are searching for rows that match. Let's take ICD-9 286.53 for example. That code maps to concept_id 0, so if we try to pull all conditions that match just the concept_id of 0 we'll pull all condtions that have no match in the SNOMED standard vocabulary. That's not what we want! So we need to search the condition_source_value field for matches on this code instead.
10
+ #
11
+ # But there's another, more complicated problem. Say we're looking for ICD-9 289.81. OMOP maps this to concept_id 432585. OMOP also maps 20 other conditions, 6 of which are other ICD-9 codes, to this same concept_id. So if we look for ICD-9s that have a non-zero condition_condept_id, we might pull up conditions that match on concept_id, but aren't the same exact code as the one we're looking for.
12
+ #
13
+ # My coworker came up with a nice, gneralized query that checks for matching concept_ids and matching source_code values. This class encapsulates that query.
14
+ #
15
+ # Subclasses must provide the following methods:
16
+ # * table
17
+ # * The CDM table name where the criterion will fetch its rows
18
+ # * e.g. for ICD-9, this would be condition_occurrence
19
+ # * concept_column
20
+ # * Name of the column in the table that stores a concept_id related to the criterion
21
+ # * e.g. for ICD-9, this would be condition_concept_id
22
+ # * source_column
23
+ # * Name of the column in the table that stores the "raw" value related to the criterion
24
+ # * e.g. for ICD-9, this would be condition_source_value
25
+ # * vocabulary_id
26
+ # * The vocabulary ID of the source vocabulary for the criterion
27
+ # * e.g. for ICD-9, a value of 2 (for ICD-9-CM)
28
+ class SourceVocabularyNode < Node
29
+ def query(db)
30
+ db.from(table_name)
31
+ .join(:vocabulary__source_to_concept_map___scm, scm__target_concept_id: table_concept_column)
32
+ .where(Sequel.expr(scm__source_code: values, scm__source_vocabulary_id: vocabulary_id).&(Sequel.expr(scm__source_code: table_source_column)))
33
+ end
34
+
35
+ def types
36
+ [table]
37
+ end
38
+
39
+ private
40
+ def table_name
41
+ @table_name ||= make_table_name(table)
42
+ end
43
+
44
+ def table_concept_column
45
+ "tab__#{concept_column}".to_sym
46
+ end
47
+
48
+ def table_source_column
49
+ "tab__#{source_column}".to_sym
50
+ end
51
+ end
52
+ end
53
+ end
54
+