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,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,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,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,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,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,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
|
+
|