conceptql 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/CHANGELOG.md +226 -117
- data/LICENSE +21 -0
- data/README.md +35 -83
- data/bin/conceptql +2 -1
- data/bin/conceptql_utils +6 -0
- data/config/multiple_vocabularies.csv +4 -0
- data/config/provenance.yml +489 -0
- data/config/race.yml +50 -0
- data/config/vocabularies.csv +87 -0
- data/lib/conceptql/behaviors/code_lister.rb +23 -0
- data/lib/conceptql/behaviors/drugish.rb +17 -0
- data/lib/conceptql/behaviors/labish.rb +19 -0
- data/lib/conceptql/behaviors/metadatable.rb +226 -73
- data/lib/conceptql/behaviors/provenanceable.rb +26 -0
- data/lib/conceptql/behaviors/single_event.rb +11 -0
- data/lib/conceptql/behaviors/timeless.rb +11 -0
- data/lib/conceptql/behaviors/utilizable.rb +206 -0
- data/lib/conceptql/behaviors/windowable.rb +27 -0
- data/lib/conceptql/cli.rb +117 -108
- data/lib/conceptql/columnizer.rb +75 -0
- data/lib/conceptql/data_model/base.rb +462 -0
- data/lib/conceptql/data_model/gdm.rb +155 -0
- data/lib/conceptql/data_model/gdm_wide.rb +42 -0
- data/lib/conceptql/data_model/omopv4_plus.rb +12 -0
- data/lib/conceptql/data_model.rb +30 -0
- data/lib/conceptql/database.rb +214 -0
- data/lib/conceptql/date_adjuster.rb +66 -18
- data/lib/conceptql/diagram.rb +243 -0
- data/lib/conceptql/lexicon/lexicon_gdm.rb +47 -0
- data/lib/conceptql/lexicon/lexicon_no_db.rb +49 -0
- data/lib/conceptql/lexicon/lexicon_ohdsi.rb +63 -0
- data/lib/conceptql/lexicon/lexicon_strategy.rb +116 -0
- data/lib/conceptql/lexicon.rb +111 -0
- data/lib/conceptql/nodifier.rb +65 -24
- data/lib/conceptql/operators/after.rb +34 -14
- data/lib/conceptql/operators/any_overlap.rb +8 -6
- data/lib/conceptql/operators/before.rb +34 -10
- data/lib/conceptql/operators/binary_operator_operator.rb +66 -23
- data/lib/conceptql/operators/casting_operator.rb +51 -36
- data/lib/conceptql/operators/co_reported.rb +111 -0
- data/lib/conceptql/operators/concurrent_within.rb +69 -0
- data/lib/conceptql/operators/contains.rb +12 -8
- data/lib/conceptql/operators/count.rb +17 -9
- data/lib/conceptql/operators/date_range.rb +48 -14
- data/lib/conceptql/operators/death.rb +14 -4
- data/lib/conceptql/operators/dedup.rb +31 -0
- data/lib/conceptql/operators/during.rb +7 -12
- data/lib/conceptql/operators/episode.rb +146 -0
- data/lib/conceptql/operators/equal.rb +9 -3
- data/lib/conceptql/operators/ethnicity.rb +49 -0
- data/lib/conceptql/operators/except.rb +19 -12
- data/lib/conceptql/operators/filter.rb +23 -8
- data/lib/conceptql/operators/first.rb +16 -3
- data/lib/conceptql/operators/from.rb +52 -4
- data/lib/conceptql/operators/gender.rb +34 -11
- data/lib/conceptql/operators/hospice.rb +19 -0
- data/lib/conceptql/operators/hospitalization.rb +19 -0
- data/lib/conceptql/operators/information_periods.rb +36 -0
- data/lib/conceptql/operators/invalid.rb +35 -0
- data/lib/conceptql/operators/last.rb +16 -3
- data/lib/conceptql/operators/match.rb +91 -0
- data/lib/conceptql/operators/multiple_vocabularies.rb +172 -0
- data/lib/conceptql/operators/numeric.rb +65 -23
- data/lib/conceptql/operators/numeric_filter.rb +68 -0
- data/lib/conceptql/operators/occurrence.rb +153 -32
- data/lib/conceptql/operators/one_in_two_out.rb +237 -38
- data/lib/conceptql/operators/operator.rb +624 -178
- data/lib/conceptql/operators/pass_thru.rb +17 -3
- data/lib/conceptql/operators/person.rb +18 -6
- data/lib/conceptql/operators/person_filter.rb +9 -3
- data/lib/conceptql/operators/place_of_service_filter.rb +43 -0
- data/lib/conceptql/operators/provenance.rb +55 -0
- data/lib/conceptql/operators/provider_filter.rb +62 -0
- data/lib/conceptql/operators/race.rb +55 -7
- data/lib/conceptql/operators/read.rb +168 -0
- data/lib/conceptql/operators/recall.rb +49 -14
- data/lib/conceptql/operators/snf.rb +19 -0
- data/lib/conceptql/operators/sum.rb +29 -12
- data/lib/conceptql/operators/temporal_operator.rb +108 -16
- data/lib/conceptql/operators/time_window.rb +20 -32
- data/lib/conceptql/operators/trim_date.rb +71 -0
- data/lib/conceptql/operators/trim_date_end.rb +28 -40
- data/lib/conceptql/operators/trim_date_start.rb +28 -41
- data/lib/conceptql/operators/union.rb +67 -6
- data/lib/conceptql/operators/vocabulary.rb +151 -0
- data/lib/conceptql/paths.rb +41 -0
- data/lib/conceptql/query.rb +191 -26
- data/lib/conceptql/query_modifiers/gdm/admission_date_query_modifier.rb +46 -0
- data/lib/conceptql/query_modifiers/gdm/drug_query_modifier.rb +54 -0
- data/lib/conceptql/query_modifiers/gdm/lab_query_modifier.rb +57 -0
- data/lib/conceptql/query_modifiers/gdm/pos_query_modifier.rb +46 -0
- data/lib/conceptql/query_modifiers/gdm/provenance_query_modifier.rb +45 -0
- data/lib/conceptql/query_modifiers/gdm/provider_query_modifier.rb +53 -0
- data/lib/conceptql/query_modifiers/gdm_wide/admission_date_query_modifier.rb +42 -0
- data/lib/conceptql/query_modifiers/gdm_wide/drug_query_modifier.rb +52 -0
- data/lib/conceptql/query_modifiers/gdm_wide/lab_query_modifier.rb +55 -0
- data/lib/conceptql/query_modifiers/gdm_wide/pos_query_modifier.rb +37 -0
- data/lib/conceptql/query_modifiers/gdm_wide/provenance_query_modifier.rb +38 -0
- data/lib/conceptql/query_modifiers/omopv4_plus/admission_date_query_modifier.rb +45 -0
- data/lib/conceptql/query_modifiers/omopv4_plus/drug_query_modifier.rb +80 -0
- data/lib/conceptql/query_modifiers/omopv4_plus/pos_query_modifier.rb +47 -0
- data/lib/conceptql/query_modifiers/omopv4_plus/provenance_query_modifier.rb +45 -0
- data/lib/conceptql/query_modifiers/omopv4_plus/provider_query_modifier.rb +26 -0
- data/lib/conceptql/query_modifiers/query_modifier.rb +20 -0
- data/lib/conceptql/rdbms/duckdb.rb +25 -0
- data/lib/conceptql/rdbms/generic.rb +120 -0
- data/lib/conceptql/rdbms/postgres.rb +51 -0
- data/lib/conceptql/rdbms/spark.rb +22 -0
- data/lib/conceptql/rdbms.rb +22 -0
- data/lib/conceptql/scope.rb +481 -31
- data/lib/conceptql/sql_formatters/formatter.rb +25 -0
- data/lib/conceptql/sql_formatters.rb +22 -0
- data/lib/conceptql/utils.rb +113 -0
- data/lib/conceptql/utils_cli.rb +17 -0
- data/lib/conceptql/version.rb +3 -1
- data/lib/conceptql/visitors/metadata_extractor.rb +54 -0
- data/lib/conceptql/vocabularies/behaviors/costish.rb +24 -0
- data/lib/conceptql/vocabularies/behaviors/gdmish.rb +19 -0
- data/lib/conceptql/vocabularies/behaviors/omopish.rb +32 -0
- data/lib/conceptql/vocabularies/dynamic_vocabularies.rb +63 -0
- data/lib/conceptql/vocabularies/entry.rb +138 -0
- data/lib/conceptql/vocabularies/vocabulary_class_factory.rb +85 -0
- data/lib/conceptql/window/base.rb +26 -0
- data/lib/conceptql/window/date_range.rb +25 -0
- data/lib/conceptql/window/table.rb +116 -0
- data/lib/conceptql/window.rb +72 -0
- data/lib/conceptql.rb +65 -2
- data/lib/sequel/extensions/pg_ctas_explain.rb +22 -0
- data/lib/sequel/extensions/pg_vacuum_table.rb +23 -0
- data/schemas/gdm.yml +882 -0
- data/schemas/gdm_wide.yml +757 -0
- data/schemas/ohdsi_vocabs.yml +143 -0
- data/schemas/omopv4_plus.yml +729 -0
- data/schemas/omopv4_plus_more_ignorables.tsv +13 -0
- metadata +267 -207
- data/.gitignore +0 -23
- data/Gemfile +0 -4
- data/Guardfile +0 -56
- data/LICENSE.txt +0 -22
- data/Rakefile +0 -1
- data/conceptql.gemspec +0 -32
- data/doc/ConceptQL Specification (alpha).pdf +0 -0
- data/doc/diagram_0.png +0 -0
- data/doc/implementation_notes.md +0 -39
- data/doc/spec.md +0 -1344
- data/lib/conceptql/behaviors/debuggable.rb +0 -70
- data/lib/conceptql/behaviors/dottable.rb +0 -103
- data/lib/conceptql/behaviors/preppable.rb +0 -20
- data/lib/conceptql/converter.rb +0 -65
- data/lib/conceptql/debugger.rb +0 -48
- data/lib/conceptql/fake_grapher.rb +0 -20
- data/lib/conceptql/graph.rb +0 -52
- data/lib/conceptql/graph_nodifier.rb +0 -203
- data/lib/conceptql/logger.rb +0 -10
- data/lib/conceptql/operators/complement.rb +0 -38
- data/lib/conceptql/operators/concept.rb +0 -70
- data/lib/conceptql/operators/condition_type.rb +0 -71
- data/lib/conceptql/operators/cpt.rb +0 -25
- data/lib/conceptql/operators/drug_type_concept.rb +0 -21
- data/lib/conceptql/operators/from_seer_visits.rb +0 -23
- data/lib/conceptql/operators/hcpcs.rb +0 -25
- data/lib/conceptql/operators/icd10.rb +0 -28
- data/lib/conceptql/operators/icd9.rb +0 -28
- data/lib/conceptql/operators/icd9_procedure.rb +0 -25
- data/lib/conceptql/operators/intersect.rb +0 -34
- data/lib/conceptql/operators/loinc.rb +0 -25
- data/lib/conceptql/operators/medcode.rb +0 -28
- data/lib/conceptql/operators/medcode_procedure.rb +0 -27
- data/lib/conceptql/operators/ndc.rb +0 -29
- data/lib/conceptql/operators/observation_by_enttype.rb +0 -29
- data/lib/conceptql/operators/observation_period.rb +0 -30
- data/lib/conceptql/operators/overlapped_by.rb +0 -23
- data/lib/conceptql/operators/overlaps.rb +0 -19
- data/lib/conceptql/operators/place_of_service_code.rb +0 -24
- data/lib/conceptql/operators/procedure_occurrence.rb +0 -25
- data/lib/conceptql/operators/prodcode.rb +0 -29
- data/lib/conceptql/operators/rxnorm.rb +0 -24
- data/lib/conceptql/operators/snomed.rb +0 -24
- data/lib/conceptql/operators/snomed_condition.rb +0 -26
- data/lib/conceptql/operators/source_vocabulary_operator.rb +0 -58
- data/lib/conceptql/operators/standard_vocabulary_operator.rb +0 -45
- data/lib/conceptql/operators/started_by.rb +0 -22
- data/lib/conceptql/operators/to_seer_visits.rb +0 -24
- data/lib/conceptql/operators/visit.rb +0 -15
- data/lib/conceptql/operators/visit_occurrence.rb +0 -30
- data/lib/conceptql/tree.rb +0 -53
- data/lib/conceptql/utils/temp_table.rb +0 -72
- data/spec/conceptql/behaviors/dottable_spec.rb +0 -99
- data/spec/conceptql/converter_spec.rb +0 -51
- data/spec/conceptql/date_adjuster_spec.rb +0 -68
- data/spec/conceptql/operators/after_spec.rb +0 -16
- data/spec/conceptql/operators/before_spec.rb +0 -16
- data/spec/conceptql/operators/casting_operator_spec.rb +0 -68
- data/spec/conceptql/operators/complement_spec.rb +0 -15
- data/spec/conceptql/operators/concept_spec.rb +0 -40
- data/spec/conceptql/operators/condition_type_spec.rb +0 -128
- data/spec/conceptql/operators/contains_spec.rb +0 -19
- data/spec/conceptql/operators/cpt_spec.rb +0 -29
- data/spec/conceptql/operators/date_range_spec.rb +0 -33
- data/spec/conceptql/operators/death_spec.rb +0 -10
- data/spec/conceptql/operators/during_spec.rb +0 -30
- data/spec/conceptql/operators/except_spec.rb +0 -15
- data/spec/conceptql/operators/first_spec.rb +0 -35
- data/spec/conceptql/operators/from_spec.rb +0 -13
- data/spec/conceptql/operators/gender_spec.rb +0 -27
- data/spec/conceptql/operators/hcpcs_spec.rb +0 -29
- data/spec/conceptql/operators/icd10_spec.rb +0 -34
- data/spec/conceptql/operators/icd9_procedure_spec.rb +0 -29
- data/spec/conceptql/operators/icd9_spec.rb +0 -34
- data/spec/conceptql/operators/intersect_spec.rb +0 -28
- data/spec/conceptql/operators/last_spec.rb +0 -36
- data/spec/conceptql/operators/loinc_spec.rb +0 -29
- data/spec/conceptql/operators/medcode_procedure_spec.rb +0 -34
- data/spec/conceptql/operators/medcode_spec.rb +0 -34
- data/spec/conceptql/operators/observation_period_spec.rb +0 -10
- data/spec/conceptql/operators/occurrence_spec.rb +0 -87
- data/spec/conceptql/operators/overlapped_by_spec.rb +0 -32
- data/spec/conceptql/operators/overlaps_spec.rb +0 -21
- data/spec/conceptql/operators/person_filter_spec.rb +0 -15
- data/spec/conceptql/operators/person_spec.rb +0 -10
- data/spec/conceptql/operators/place_of_service_code_spec.rb +0 -24
- data/spec/conceptql/operators/procedure_occurrence_spec.rb +0 -10
- data/spec/conceptql/operators/prodcode_spec.rb +0 -35
- data/spec/conceptql/operators/query_double.rb +0 -20
- data/spec/conceptql/operators/query_double_spec.rb +0 -7
- data/spec/conceptql/operators/race_spec.rb +0 -21
- data/spec/conceptql/operators/rxnorm_spec.rb +0 -29
- data/spec/conceptql/operators/snomed_spec.rb +0 -29
- data/spec/conceptql/operators/source_vocabulary_operator_spec.rb +0 -35
- data/spec/conceptql/operators/standard_vocabulary_operator_spec.rb +0 -35
- data/spec/conceptql/operators/started_by_spec.rb +0 -22
- data/spec/conceptql/operators/temporal_operator_spec.rb +0 -51
- data/spec/conceptql/operators/time_window_spec.rb +0 -77
- data/spec/conceptql/operators/union_spec.rb +0 -21
- data/spec/conceptql/operators/visit_occurrence_spec.rb +0 -10
- data/spec/conceptql/query_spec.rb +0 -22
- data/spec/conceptql/tree_spec.rb +0 -50
- data/spec/doubles/stream_for_casting_double.rb +0 -9
- data/spec/doubles/stream_for_occurrence_double.rb +0 -25
- data/spec/doubles/stream_for_temporal_double.rb +0 -6
- data/spec/spec_helper.rb +0 -102
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "operator"
|
|
4
|
+
|
|
5
|
+
module ConceptQL
|
|
6
|
+
module Operators
|
|
7
|
+
class Vocabulary < Operator
|
|
8
|
+
include ConceptQL::Behaviors::Windowable
|
|
9
|
+
include ConceptQL::Behaviors::CodeLister
|
|
10
|
+
|
|
11
|
+
category "Select by Clinical Codes"
|
|
12
|
+
basic_type :selection
|
|
13
|
+
desc "Selects records based on the given vocabulary."
|
|
14
|
+
validate_no_upstreams
|
|
15
|
+
validate_at_least_one_argument
|
|
16
|
+
validate_codes_match
|
|
17
|
+
|
|
18
|
+
def source_table
|
|
19
|
+
domain_map(op_name)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def table
|
|
23
|
+
domain_map(op_name)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def domain
|
|
27
|
+
domain_map(op_name)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def domains(_db)
|
|
31
|
+
vocab_entry.predominant_domains.map(&:to_sym)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def additional_validation(db, opts = {})
|
|
35
|
+
return unless add_warnings?(db, opts) && !select_all?
|
|
36
|
+
|
|
37
|
+
args = arguments.dup
|
|
38
|
+
args -= bad_arguments
|
|
39
|
+
|
|
40
|
+
missing_args = args - dm.known_codes(db, vocabulary_id, args)
|
|
41
|
+
|
|
42
|
+
return if missing_args.empty?
|
|
43
|
+
|
|
44
|
+
add_warning("unknown code(s)", *missing_args)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def describe_codes(db, codes)
|
|
48
|
+
return [ [ "*", "ALL CODES" ] ] if select_all?
|
|
49
|
+
|
|
50
|
+
found_codes = dm.concepts_to_codes(db, vocabulary_id, codes)
|
|
51
|
+
|
|
52
|
+
found_codes + (codes - found_codes.map(&:first)).zip([])
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def filter_clause(db)
|
|
56
|
+
filter_clause = Sequel[where_clause(db)]
|
|
57
|
+
if (ex_clause = exclusion_clause(db)).present?
|
|
58
|
+
filter_clause &= ~Sequel[ex_clause]
|
|
59
|
+
end
|
|
60
|
+
filter_clause
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def criterion_table
|
|
64
|
+
dm.table_by_domain(domain)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# The condition that selects this vocabulary's rows. where_clause
|
|
68
|
+
# includes it, so Union can name every merged vocabulary in one IN list.
|
|
69
|
+
def vocabulary_condition
|
|
70
|
+
{ clinical_code_vocabulary_id: vocabulary_id }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def query(db)
|
|
76
|
+
ds = db[criterion_table]
|
|
77
|
+
|
|
78
|
+
ds = ds.where(filter_clause(db))
|
|
79
|
+
|
|
80
|
+
unless (more_columns = additional_columns(db)).empty?
|
|
81
|
+
ds = ds.select_append(*more_columns)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
ds
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def exclusion_clause(_db)
|
|
88
|
+
{}
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def additional_columns(_db)
|
|
92
|
+
[]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def where_clause(db)
|
|
96
|
+
# We're probably going to partition the observations table by clinical_code_vocabulary_id,
|
|
97
|
+
# for Spark, so this will create queries that leverage that partitioning
|
|
98
|
+
# and it shouldn't hurt for PostgreSQL performance
|
|
99
|
+
wheres = vocabulary_condition
|
|
100
|
+
|
|
101
|
+
unless select_all?
|
|
102
|
+
concept_ids = dm.concept_ids(db, vocabulary_id, arguments)
|
|
103
|
+
|
|
104
|
+
wheres = wheres.merge({ clinical_code_concept_id: concept_ids })
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
wheres
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def query_cols
|
|
111
|
+
dm.table_columns(source_table)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def select_all?
|
|
115
|
+
arguments.include?("*")
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def preferred_name
|
|
119
|
+
vocab_entry.short_name || vocab_entry.omopv5_id
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Defined so that bad_arguments can check for bad codes
|
|
123
|
+
def code_regexp
|
|
124
|
+
unless defined?(@code_regexp)
|
|
125
|
+
@code_regexp = nil
|
|
126
|
+
|
|
127
|
+
if (reg_str = vocab_entry.format_regexp)
|
|
128
|
+
@code_regexp = Regexp.union(Regexp.new(reg_str, Regexp::IGNORECASE), /\A\*\Z/)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
@code_regexp
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def vocabulary_id
|
|
135
|
+
@vocabulary_id ||= translated_vocabulary_id
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def translated_vocabulary_id
|
|
139
|
+
vocab_entry.omopv5_vocabulary_id || op_name
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def domain_map(_v_id)
|
|
143
|
+
(vocab_entry.domain || :condition_occurrence).to_sym
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def table_is_missing?(db)
|
|
147
|
+
dm.table_is_missing?(db)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Pathname is core only from Ruby 4.0; on 3.x a clean `gem install` (no Bundler
|
|
4
|
+
# preloading it) fails in ConceptQL.root without this. RuboCop targets 4.0.
|
|
5
|
+
require "pathname"
|
|
6
|
+
|
|
7
|
+
module ConceptQL
|
|
8
|
+
class << self
|
|
9
|
+
def root
|
|
10
|
+
Pathname.new(__dir__).parent.parent.expand_path
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def schemas_dir
|
|
14
|
+
root / "schemas"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def config_dir
|
|
18
|
+
root / "config"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def vocabularies_file_path
|
|
22
|
+
ConceptQL.config_dir / "vocabularies.csv"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def multiple_vocabularies_file_path
|
|
26
|
+
ConceptQL.config_dir / "multiple_vocabularies.csv"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def custom_vocabularies_file_path
|
|
30
|
+
Pathname.new(ENV["CONCEPTQL_CUSTOM_VOCABULARIES"] || ConceptQL.config_dir / "vocabularies.custom.csv")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def custom_multiple_vocabularies_file_path
|
|
34
|
+
Pathname.new(ENV["CONCEPTQL_CUSTOM_MULTIPLE_VOCABULARIES"] || ConceptQL.config_dir / "multiple_vocabularies.custom.csv")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def race_file
|
|
38
|
+
ConceptQL.config_dir / "race.yml"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
data/lib/conceptql/query.rb
CHANGED
|
@@ -1,50 +1,215 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "forwardable"
|
|
5
|
+
require_relative "scope"
|
|
6
|
+
require_relative "nodifier"
|
|
7
|
+
require_relative "sql_formatters"
|
|
5
8
|
|
|
6
9
|
module ConceptQL
|
|
7
10
|
class Query
|
|
11
|
+
class QueryError < StandardError
|
|
12
|
+
def message
|
|
13
|
+
return super unless cause
|
|
14
|
+
|
|
15
|
+
og_message = if cause.respond_to?(:full_message)
|
|
16
|
+
cause.full_message
|
|
17
|
+
else
|
|
18
|
+
([ cause.message ] + cause.backtrace).join("\n")
|
|
19
|
+
end
|
|
20
|
+
[ super, "OG ERROR:", og_message ].join("\n")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Raised by #code_list when the statement has validation errors: the
|
|
25
|
+
# operator tree of an invalid statement can have missing operands, and a
|
|
26
|
+
# partial code list would look complete. #errors is the errors hash of
|
|
27
|
+
# #scope_annotate (operator name => list of [message, *details]).
|
|
28
|
+
class InvalidStatementError < QueryError
|
|
29
|
+
attr_reader :statement, :errors
|
|
30
|
+
|
|
31
|
+
def initialize(statement, errors)
|
|
32
|
+
@statement = statement
|
|
33
|
+
@errors = errors
|
|
34
|
+
super([ "Invalid ConceptQL statement, so it has no code list. Validation errors:",
|
|
35
|
+
*errors.flat_map { |op, op_errors| op_errors.map { |e| " #{op}: #{describe(e)}" } },
|
|
36
|
+
"Statement: #{statement.inspect}" ].join("\n"))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def describe(error)
|
|
42
|
+
message, *details = error
|
|
43
|
+
details.empty? ? message.to_s : "#{message} (#{details.flatten.map(&:to_s).join(', ')})"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
8
47
|
extend Forwardable
|
|
9
|
-
def_delegators :
|
|
48
|
+
def_delegators :query, :all, :count, :execute, :order
|
|
49
|
+
def_delegators :cdb, :db
|
|
50
|
+
|
|
51
|
+
attr_reader :statement
|
|
52
|
+
|
|
53
|
+
def initialize(cdb, statement, opts = {})
|
|
54
|
+
@cdb = cdb
|
|
55
|
+
@statement, opts = extract_statement(statement, opts)
|
|
56
|
+
@opts = opts
|
|
57
|
+
opts[:algorithm_fetcher] ||= proc do |alg|
|
|
58
|
+
statement, description = db[:concepts].where(concept_id: alg).get(%i[statement label])
|
|
59
|
+
statement = JSON.parse(statement) if statement.is_a?(String)
|
|
60
|
+
[ statement, description ]
|
|
61
|
+
end
|
|
62
|
+
@nodifier = opts[:nodifier] || Nodifier.new(cdb, { database_type: cdb.database_type }.merge(opts))
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def analyze
|
|
66
|
+
query(explain: true, analyze: true).analyze
|
|
67
|
+
end
|
|
10
68
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
@db = db
|
|
14
|
-
@db.extend_datasets(ConceptQL::Behaviors::Preppable)
|
|
15
|
-
@statement = statement
|
|
16
|
-
@tree = tree
|
|
69
|
+
def query(opts = {})
|
|
70
|
+
(@queries ||= {})[opts] ||= nodifier.scope.with_ctes(operator, db, opts)
|
|
17
71
|
end
|
|
18
72
|
|
|
19
|
-
def
|
|
20
|
-
|
|
73
|
+
def query_cols(opts = {})
|
|
74
|
+
cols = operator.dynamic_columns
|
|
75
|
+
if opts[:cast]
|
|
76
|
+
cols = query_cols.to_h do |column|
|
|
77
|
+
[ column, operator.cast_column(column) ]
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
cols
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def column_names
|
|
84
|
+
query_cols.is_a?(Hash) ? query_cols.keys : query_cols
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def sql(*args)
|
|
88
|
+
sql_statements(*args).values.join(";\n")
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def sql_statements(*args)
|
|
92
|
+
stmts = query.sql_statements
|
|
93
|
+
|
|
94
|
+
if args.include?(:create_tables)
|
|
95
|
+
sql = stmts.delete(:query)
|
|
96
|
+
drop_stmts = []
|
|
97
|
+
if args.include?(:drop_tables)
|
|
98
|
+
drop_stmts = stmts.map do |name, _|
|
|
99
|
+
[ [ "drop", name ].join("_"), db.send(:drop_table_sql, name, if_exists: true) ]
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
create_stmts = stmts.map do |name, sql|
|
|
103
|
+
[ name, db.send(:create_table_as_sql, name, sql, {}) ]
|
|
104
|
+
end
|
|
105
|
+
stmts = drop_stmts + create_stmts
|
|
106
|
+
stmts.push([ :query, sql ])
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
if args.include?(:formatted)
|
|
110
|
+
stmts = stmts.map do |name, sql|
|
|
111
|
+
[ name, format(sql) ]
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
Hash[stmts]
|
|
115
|
+
rescue StandardError
|
|
116
|
+
raise QueryError, "Failed to generate SQL for #{statement.inspect}"
|
|
21
117
|
end
|
|
22
118
|
|
|
23
|
-
def
|
|
24
|
-
|
|
119
|
+
def annotate(opts = {})
|
|
120
|
+
operator.annotate(db, opts)
|
|
25
121
|
end
|
|
26
122
|
|
|
27
|
-
def
|
|
28
|
-
|
|
123
|
+
def accept(visitor, _opts = {})
|
|
124
|
+
operator.accept(visitor)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def extract_metadata
|
|
128
|
+
visitor = ConceptQL::Visitors::MetadataExtractor.new
|
|
129
|
+
operator.accept(visitor)
|
|
130
|
+
visitor.results
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def scope_annotate(opts = {})
|
|
134
|
+
annotate(opts)
|
|
135
|
+
nodifier.scope.annotation
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def domains
|
|
139
|
+
operator.domains(db)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def events_per_patient
|
|
143
|
+
operator.events_per_patient
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def multiple_vocabularies
|
|
147
|
+
operator.multiple_vocabularies
|
|
29
148
|
end
|
|
30
149
|
|
|
31
150
|
def operator
|
|
32
|
-
@operator ||=
|
|
151
|
+
@operator ||= if statement.is_a?(Array)
|
|
152
|
+
if statement.first.is_a?(Array)
|
|
153
|
+
Operators::Invalid.new(nodifier, "invalid", errors: [ [ "incomplete statement" ] ])
|
|
154
|
+
else
|
|
155
|
+
nodifier.create(*statement)
|
|
156
|
+
end
|
|
157
|
+
else
|
|
158
|
+
Operators::Invalid.new(nodifier, "invalid",
|
|
159
|
+
errors: [ [ "invalid root operator", statement.inspect ] ])
|
|
160
|
+
end
|
|
33
161
|
end
|
|
34
162
|
|
|
35
|
-
|
|
36
|
-
|
|
163
|
+
def rdbms
|
|
164
|
+
operator.rdbms
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Raises InvalidStatementError if the statement has validation errors.
|
|
168
|
+
# Warnings do not raise.
|
|
169
|
+
def code_list(_ignored_db = nil)
|
|
170
|
+
errors = validation_errors
|
|
171
|
+
raise InvalidStatementError.new(statement, errors) unless errors.empty?
|
|
172
|
+
|
|
173
|
+
operator.code_list(db).uniq
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# The errors that #scope_annotate reports, the same check that callers use
|
|
177
|
+
# to decide whether a statement is valid. A fresh query runs it, because
|
|
178
|
+
# operators memoize #annotate and #validate, and this query's later
|
|
179
|
+
# #annotate calls must not get those results.
|
|
180
|
+
def validation_errors
|
|
181
|
+
Query.new(cdb, statement, @opts).scope_annotate(skip_counts: true)[:errors]
|
|
182
|
+
end
|
|
37
183
|
|
|
38
|
-
def
|
|
39
|
-
|
|
184
|
+
def drop_temp_tables(opts = {})
|
|
185
|
+
query.drop_temp_tables(opts)
|
|
40
186
|
end
|
|
41
187
|
|
|
42
|
-
|
|
43
|
-
|
|
188
|
+
private
|
|
189
|
+
|
|
190
|
+
attr_reader :cdb, :nodifier
|
|
191
|
+
|
|
192
|
+
def extract_statement(stmt, opts)
|
|
193
|
+
if !stmt.is_a?(Array)
|
|
194
|
+
raise "Improper ConceptQL statement: Expected an Array, got a #{stmt.class}"
|
|
195
|
+
elsif stmt.first.to_s == "window"
|
|
196
|
+
raise "window operator needs a hash as the last item of the array." unless stmt.last.is_a?(Hash)
|
|
197
|
+
unless stmt.length == 3
|
|
198
|
+
raise "window operator needs a ConceptQL statement followed by a hash as the last item of the array."
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
opts[:scope_opts] = (opts[:scope_opts] || {})
|
|
202
|
+
opts[:scope_opts].merge!(window_opts: stmt.last.merge(cdb: cdb))
|
|
203
|
+
extract_statement(stmt[1], opts)
|
|
204
|
+
elsif stmt.length == 1 && stmt.first.is_a?(Array)
|
|
205
|
+
extract_statement(stmt.first, opts)
|
|
206
|
+
else
|
|
207
|
+
[ stmt, opts ]
|
|
208
|
+
end
|
|
44
209
|
end
|
|
45
210
|
|
|
46
|
-
def
|
|
47
|
-
|
|
211
|
+
def format(sql)
|
|
212
|
+
SqlFormatters.format(sql, rdbms)
|
|
48
213
|
end
|
|
49
214
|
end
|
|
50
215
|
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../query_modifier"
|
|
4
|
+
|
|
5
|
+
module ConceptQL
|
|
6
|
+
module QueryModifiers
|
|
7
|
+
module Gdm
|
|
8
|
+
class AdmissionDateQueryModifier < QueryModifier
|
|
9
|
+
def self.provided_columns
|
|
10
|
+
%i[
|
|
11
|
+
admission_date
|
|
12
|
+
discharge_date
|
|
13
|
+
]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.has_required_columns?(cols)
|
|
17
|
+
needed = [ :collection_id ]
|
|
18
|
+
found = needed & cols
|
|
19
|
+
needed == found
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def modified_query
|
|
23
|
+
return query unless dm.table_cols(source_table).include?(:collection_id)
|
|
24
|
+
|
|
25
|
+
query.from_self(alias: :cc)
|
|
26
|
+
.left_join(:collections, { id: :collection_id }, table_alias: :co)
|
|
27
|
+
.left_join(:admission_details, { id: :admission_detail_id }, table_alias: :ad)
|
|
28
|
+
.select_all(:cc)
|
|
29
|
+
.select_append(Sequel.function(:coalesce, Sequel[:ad][:admission_date],
|
|
30
|
+
Sequel[:cc][:start_date]).as(:admission_date))
|
|
31
|
+
.select_append(Sequel.function(:coalesce, Sequel[:ad][:discharge_date], Sequel[:cc][:end_date],
|
|
32
|
+
Sequel[:cc][:start_date]).as(:discharge_date))
|
|
33
|
+
.from_self
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def domain
|
|
39
|
+
op.domain
|
|
40
|
+
rescue StandardError
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../query_modifier"
|
|
4
|
+
|
|
5
|
+
module ConceptQL
|
|
6
|
+
module QueryModifiers
|
|
7
|
+
module Gdm
|
|
8
|
+
class DrugQueryModifier < QueryModifier
|
|
9
|
+
def self.provided_columns
|
|
10
|
+
%i[
|
|
11
|
+
drug_name
|
|
12
|
+
drug_amount
|
|
13
|
+
drug_amount_units
|
|
14
|
+
drug_quantity
|
|
15
|
+
drug_strength_source_value
|
|
16
|
+
drug_days_supply
|
|
17
|
+
]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.has_required_columns?(cols)
|
|
21
|
+
needed = [ :drug_exposure_detail_id ]
|
|
22
|
+
found = needed & cols
|
|
23
|
+
needed == found
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def modified_query
|
|
27
|
+
return query unless dm.table_cols(source_table).include?(:drug_exposure_detail_id)
|
|
28
|
+
|
|
29
|
+
# TODO: Determine what actual columns to include for drug exposures under
|
|
30
|
+
query.from_self(alias: :cc)
|
|
31
|
+
.left_join(Sequel[:drug_exposure_details].as(:de), Sequel[:cc][:drug_exposure_detail_id] => Sequel[:de][:id])
|
|
32
|
+
.left_join(dm.name_concepts_table(query.db).as(:dose_con), Sequel[:de][:dose_unit_concept_id] => Sequel[:dose_con][:id])
|
|
33
|
+
.left_join(dm.name_concepts_table(query.db).as(:ing_con), Sequel[:cc][:clinical_code_concept_id] => Sequel[:ing_con][:id])
|
|
34
|
+
.select_all(:cc)
|
|
35
|
+
.select_append(Sequel[:de][:dose_value].as(:drug_amount))
|
|
36
|
+
.select_append(Sequel[:dose_con][:concept_text].as(:drug_amount_units))
|
|
37
|
+
.select_append(Sequel[:ing_con][:concept_text].as(:drug_name))
|
|
38
|
+
.select_append(Sequel[:de][:days_supply].as(:drug_days_supply))
|
|
39
|
+
.select_append(Sequel[:de][:strength_source_value].as(:drug_strength_source_value))
|
|
40
|
+
.select_append(Sequel[:cc][:quantity].as(:drug_quantity))
|
|
41
|
+
.from_self
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def domain
|
|
47
|
+
op.domain
|
|
48
|
+
rescue StandardError
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../query_modifier"
|
|
4
|
+
|
|
5
|
+
module ConceptQL
|
|
6
|
+
module QueryModifiers
|
|
7
|
+
module Gdm
|
|
8
|
+
class LabQueryModifier < QueryModifier
|
|
9
|
+
def self.provided_columns
|
|
10
|
+
%i[
|
|
11
|
+
value_as_number
|
|
12
|
+
value_as_string
|
|
13
|
+
value_as_concept_id
|
|
14
|
+
value_as_concept
|
|
15
|
+
unit_concept_id
|
|
16
|
+
unit_concept
|
|
17
|
+
range_low
|
|
18
|
+
range_high
|
|
19
|
+
]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.has_required_columns?(cols)
|
|
23
|
+
needed = [ :measurement_detail_id ]
|
|
24
|
+
found = needed & cols
|
|
25
|
+
needed == found
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def modified_query
|
|
29
|
+
return query unless dm.table_cols(source_table).include?(:measurement_detail_id)
|
|
30
|
+
|
|
31
|
+
query.from_self(alias: :cc)
|
|
32
|
+
.left_join(Sequel[:measurement_details].as(:md), Sequel[:cc][:measurement_detail_id] => Sequel[:md][:id])
|
|
33
|
+
.left_join(dm.name_concepts_table(query.db).as(:unit_con), Sequel[:md][:unit_concept_id] => Sequel[:unit_con][:id])
|
|
34
|
+
.left_join(dm.name_concepts_table(query.db).as(:concept_con), Sequel[:md][:result_as_concept_id] => Sequel[:concept_con][:id])
|
|
35
|
+
.select_all(:cc)
|
|
36
|
+
.select_append(Sequel[:md][:result_as_number].as(:value_as_number))
|
|
37
|
+
.select_append(Sequel[:md][:result_as_string].as(:value_as_string))
|
|
38
|
+
.select_append(Sequel[:md][:result_as_concept_id].as(:value_as_concept_id))
|
|
39
|
+
.select_append(Sequel[:concept_con][:concept_code].as(:value_as_concept))
|
|
40
|
+
.select_append(Sequel[:md][:unit_concept_id].as(:unit_concept_id))
|
|
41
|
+
.select_append(Sequel[:unit_con][:concept_code].as(:unit_concept))
|
|
42
|
+
.select_append(Sequel[:md][:normal_range_low].as(:range_low))
|
|
43
|
+
.select_append(Sequel[:md][:normal_range_high].as(:range_high))
|
|
44
|
+
.from_self
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def domain
|
|
50
|
+
op.domain
|
|
51
|
+
rescue StandardError
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../query_modifier"
|
|
4
|
+
|
|
5
|
+
module ConceptQL
|
|
6
|
+
module QueryModifiers
|
|
7
|
+
module Gdm
|
|
8
|
+
class PoSQueryModifier < QueryModifier
|
|
9
|
+
attr_reader :join_id, :table, :source_column, :column
|
|
10
|
+
|
|
11
|
+
def self.provided_columns
|
|
12
|
+
[ :visit_source_concept_id ]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.has_required_columns?(cols)
|
|
16
|
+
needed = [ :context_id ]
|
|
17
|
+
found = needed & cols
|
|
18
|
+
needed == found
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def initialize(*args)
|
|
22
|
+
super
|
|
23
|
+
@column = :visit_source_concept_id
|
|
24
|
+
@join_id = :context_id
|
|
25
|
+
@table = :contexts
|
|
26
|
+
@source_column = :pos_concept_id
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def modified_query
|
|
30
|
+
return query unless dm.table_cols(source_table).include?(join_id)
|
|
31
|
+
|
|
32
|
+
left_alias = "tab1".to_sym
|
|
33
|
+
right_alias = "tab2".to_sym
|
|
34
|
+
|
|
35
|
+
extra_table = query.db.from(table).select(Sequel.as(source_column, column), :id)
|
|
36
|
+
ds = query.from_self(alias: left_alias)
|
|
37
|
+
ds.left_join(extra_table.as(right_alias),
|
|
38
|
+
Sequel.qualify(left_alias, join_id) => Sequel.qualify(right_alias, :id))
|
|
39
|
+
.select_all(left_alias)
|
|
40
|
+
.select_append(Sequel.qualify(right_alias, column).as(column))
|
|
41
|
+
.from_self
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../query_modifier"
|
|
4
|
+
|
|
5
|
+
module ConceptQL
|
|
6
|
+
module QueryModifiers
|
|
7
|
+
module Gdm
|
|
8
|
+
class ProvenanceQueryModifier < QueryModifier
|
|
9
|
+
RELATED_COLUMNS = %w[
|
|
10
|
+
provenance_concept_id
|
|
11
|
+
source_type_concept_id
|
|
12
|
+
].sort.map(&:to_sym)
|
|
13
|
+
|
|
14
|
+
attr_reader :db
|
|
15
|
+
|
|
16
|
+
def self.provided_columns
|
|
17
|
+
%i[
|
|
18
|
+
file_provenance_type
|
|
19
|
+
code_provenance_type
|
|
20
|
+
]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.has_required_columns?(cols)
|
|
24
|
+
!(RELATED_COLUMNS & cols).empty?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def initialize(*args)
|
|
28
|
+
super
|
|
29
|
+
@db = query.db
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def modified_query
|
|
33
|
+
return query unless self.class.has_required_columns?(dm.table_cols(source_table))
|
|
34
|
+
|
|
35
|
+
query.from_self(alias: :cc)
|
|
36
|
+
.left_join(Sequel[:contexts].as(:c), Sequel[:cc][:context_id] => Sequel[:c][:id])
|
|
37
|
+
.select_all(:cc)
|
|
38
|
+
.select_append(Sequel[:c][:source_type_concept_id].as(:file_provenance_type),
|
|
39
|
+
Sequel[:cc][:provenance_concept_id].as(:code_provenance_type))
|
|
40
|
+
.from_self
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|