conceptql 0.4.1 → 0.6.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 +4 -4
- data/CHANGELOG.md +498 -0
- data/config/vocabularies.csv +85 -87
- data/config/vocabulary_formats.sha +1 -0
- data/config/vocabulary_formats.tsv +39 -0
- data/doc/spec.md +2091 -0
- data/lib/conceptql/behaviors/metadatable.rb +15 -3
- data/lib/conceptql/behaviors/provenanceable.rb +82 -8
- data/lib/conceptql/behaviors/utilizable.rb +5 -3
- data/lib/conceptql/cli.rb +1 -1
- data/lib/conceptql/data_model/base.rb +3 -171
- data/lib/conceptql/data_model/gdm.rb +3 -1
- data/lib/conceptql/data_model.rb +20 -4
- data/lib/conceptql/database.rb +9 -2
- data/lib/conceptql/date_adjuster.rb +12 -5
- data/lib/conceptql/lexicon/lexicon_gdm.rb +4 -1
- data/lib/conceptql/lexicon/lexicon_ohdsi.rb +19 -1
- data/lib/conceptql/lexicon/lexicon_strategy.rb +58 -4
- data/lib/conceptql/lexicon.rb +218 -2
- data/lib/conceptql/nodifier.rb +7 -1
- data/lib/conceptql/one_query_counts.rb +120 -0
- data/lib/conceptql/operators/co_reported.rb +4 -31
- data/lib/conceptql/operators/count.rb +4 -0
- data/lib/conceptql/operators/death.rb +1 -1
- data/lib/conceptql/operators/dedup.rb +1 -1
- data/lib/conceptql/operators/episode.rb +10 -2
- data/lib/conceptql/operators/ethnicity.rb +0 -4
- data/lib/conceptql/operators/except.rb +5 -0
- data/lib/conceptql/operators/filter.rb +12 -2
- data/lib/conceptql/operators/hospice.rb +2 -3
- data/lib/conceptql/operators/hospitalization.rb +2 -3
- data/lib/conceptql/operators/information_periods.rb +5 -4
- data/lib/conceptql/operators/invalid.rb +7 -1
- data/lib/conceptql/operators/multiple_vocabularies.rb +19 -6
- data/lib/conceptql/operators/numeric.rb +51 -14
- data/lib/conceptql/operators/occurrence.rb +16 -4
- data/lib/conceptql/operators/one_in_two_out.rb +1 -2
- data/lib/conceptql/operators/operator.rb +66 -30
- data/lib/conceptql/operators/person.rb +1 -1
- data/lib/conceptql/operators/person_filter.rb +5 -0
- data/lib/conceptql/operators/place_of_service_filter.rb +38 -10
- data/lib/conceptql/operators/provenance.rb +13 -4
- data/lib/conceptql/operators/provider_filter.rb +5 -1
- data/lib/conceptql/operators/race.rb +2 -10
- data/lib/conceptql/operators/read.rb +1 -100
- data/lib/conceptql/operators/recall.rb +1 -1
- data/lib/conceptql/operators/snf.rb +3 -4
- data/lib/conceptql/operators/sum.rb +46 -5
- data/lib/conceptql/operators/temporal_operator.rb +6 -2
- data/lib/conceptql/operators/trim_date.rb +52 -6
- data/lib/conceptql/operators/trim_date_end.rb +11 -1
- data/lib/conceptql/operators/trim_date_start.rb +11 -1
- data/lib/conceptql/operators/union.rb +45 -1
- data/lib/conceptql/operators/vocabulary.rb +21 -4
- data/lib/conceptql/paths.rb +16 -0
- data/lib/conceptql/query.rb +78 -24
- data/lib/conceptql/query_modifiers/gdm/provider_query_modifier.rb +7 -7
- data/lib/conceptql/rdbms/generic.rb +21 -1
- data/lib/conceptql/rdbms/spark.rb +12 -0
- data/lib/conceptql/scope.rb +187 -24
- data/lib/conceptql/version.rb +1 -1
- data/lib/conceptql/vocabularies/drg_systems.rb +118 -0
- data/lib/conceptql/vocabularies/dynamic_vocabularies.rb +10 -4
- data/lib/conceptql/vocabularies/entry.rb +15 -23
- data/lib/conceptql/vocabularies/formats.rb +59 -0
- data/lib/conceptql/vocabularies/vocabulary_class_factory.rb +8 -16
- data/lib/conceptql/window/table.rb +12 -3
- metadata +9 -14
- data/lib/conceptql/data_model/omopv4_plus.rb +0 -12
- data/lib/conceptql/operators/concurrent_within.rb +0 -69
- data/lib/conceptql/query_modifiers/omopv4_plus/admission_date_query_modifier.rb +0 -45
- data/lib/conceptql/query_modifiers/omopv4_plus/drug_query_modifier.rb +0 -80
- data/lib/conceptql/query_modifiers/omopv4_plus/pos_query_modifier.rb +0 -47
- data/lib/conceptql/query_modifiers/omopv4_plus/provenance_query_modifier.rb +0 -45
- data/lib/conceptql/query_modifiers/omopv4_plus/provider_query_modifier.rb +0 -26
- data/lib/conceptql/vocabularies/behaviors/costish.rb +0 -24
- data/lib/conceptql/vocabularies/behaviors/omopish.rb +0 -32
- data/schemas/omopv4_plus.yml +0 -729
- data/schemas/omopv4_plus_more_ignorables.tsv +0 -13
|
@@ -38,6 +38,15 @@ module ConceptQL
|
|
|
38
38
|
@conceptql_spec_id = value
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
+
# Marks an operator as deprecated. It still runs. Operator#validate adds
|
|
42
|
+
# the message to its annotation as a warning, and its metadata says
|
|
43
|
+
# deprecated: true. A subclass does not inherit the mark.
|
|
44
|
+
def deprecated(message = nil)
|
|
45
|
+
return @deprecated unless message
|
|
46
|
+
|
|
47
|
+
@deprecated = message
|
|
48
|
+
end
|
|
49
|
+
|
|
41
50
|
def predominant_domains(*values)
|
|
42
51
|
return @predominant_domains if values.empty?
|
|
43
52
|
|
|
@@ -150,11 +159,12 @@ module ConceptQL
|
|
|
150
159
|
max_upstreams: @max_upstreams || 0,
|
|
151
160
|
arguments: @arguments || [],
|
|
152
161
|
options: process_options(@options),
|
|
153
|
-
|
|
162
|
+
# Strings, as JSON gives them: domains (for example death) are symbols
|
|
163
|
+
predominant_domains: (@domains || @predominant_domains || []).map(&:to_s),
|
|
154
164
|
desc: get_desc,
|
|
155
165
|
categories: @categories || [],
|
|
156
166
|
basic_type: @basic_type,
|
|
157
|
-
deprecated:
|
|
167
|
+
deprecated: (true if deprecated), # nil for an operator that is not deprecated
|
|
158
168
|
arity: arity,
|
|
159
169
|
aliases: @aliases,
|
|
160
170
|
spec_id: spec_id
|
|
@@ -176,8 +186,10 @@ module ConceptQL
|
|
|
176
186
|
end
|
|
177
187
|
end
|
|
178
188
|
|
|
189
|
+
# The desc that the operator set (no_desc sets ""), else the standard
|
|
190
|
+
# vocabulary text. A subclass does not inherit its parent's desc.
|
|
179
191
|
def get_desc
|
|
180
|
-
@
|
|
192
|
+
@desc || standard_description
|
|
181
193
|
end
|
|
182
194
|
|
|
183
195
|
def process_options(options)
|
|
@@ -1,26 +1,100 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module ConceptQL
|
|
4
|
+
# Provenance keywords select records by their file provenance
|
|
5
|
+
# (file_provenance_type) and their code provenance (code_provenance_type).
|
|
6
|
+
#
|
|
7
|
+
# A plain keyword, such as "inpatient" or "primary", names a concept in the
|
|
8
|
+
# file or the code provenance vocabulary. It keeps a record when either
|
|
9
|
+
# column is in the descendants of that concept.
|
|
10
|
+
#
|
|
11
|
+
# A keyword "<file keyword>:<code keyword>", such as
|
|
12
|
+
# "inpatient:primary_or_first", keeps a record when its file provenance is
|
|
13
|
+
# in the descendants of the file keyword AND its code provenance is in the
|
|
14
|
+
# descendants of the code keyword.
|
|
15
|
+
#
|
|
16
|
+
# Several keywords are ORed: a record is kept when any keyword keeps it.
|
|
17
|
+
#
|
|
18
|
+
# The keyword "*" keeps a record when either column is any provenance
|
|
19
|
+
# concept. Because keywords are ORed, "*" with other keywords is "*".
|
|
4
20
|
module Provenanceable
|
|
21
|
+
PAIR_SEPARATOR = ":"
|
|
22
|
+
ANY = "*"
|
|
23
|
+
|
|
5
24
|
def self.included(base)
|
|
6
25
|
base.require_column :file_provenance_type
|
|
7
26
|
base.require_column :code_provenance_type
|
|
8
27
|
end
|
|
9
28
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
29
|
+
# No keyword at all is a validation error of Provenance. It still builds
|
|
30
|
+
# the condition of "*", as an invalid pair still builds a condition.
|
|
31
|
+
def build_where_from_codes(db, codes)
|
|
32
|
+
return plain_condition(db, []) if codes.empty? || codes.include?(ANY)
|
|
33
|
+
|
|
34
|
+
pairs, plain = codes.partition { |code| pair_keyword?(code) }
|
|
35
|
+
conditions = pairs.map { |pair| pair_condition(db, pair) }
|
|
36
|
+
conditions.unshift(plain_condition(db, plain)) if plain.any?
|
|
37
|
+
return conditions.first if conditions.one?
|
|
38
|
+
|
|
39
|
+
Sequel.|(*conditions)
|
|
14
40
|
end
|
|
15
41
|
|
|
16
|
-
|
|
42
|
+
# The keywords as written that name no provenance concept. A pair is
|
|
43
|
+
# recognized only when it has one separator, a file keyword on the left
|
|
44
|
+
# and a code keyword on the right. Case is compared exactly, as the lookup
|
|
45
|
+
# compares it: the provenance vocabularies have no vocabulary_formats row
|
|
46
|
+
# that declares case-insensitivity (seed conceptql-oji ruling "b"). So
|
|
47
|
+
# "Inpatient" names no concept, and the concept codes are in lower case.
|
|
48
|
+
# "*" is known. It is not a half of a pair.
|
|
49
|
+
def find_bad_keywords(db, codes)
|
|
50
|
+
pairs, plain = (codes - [ ANY ]).partition { |code| pair_keyword?(code) }
|
|
51
|
+
known = known_keywords(db, all_provenance_types_vocab)
|
|
52
|
+
bad = plain.reject { |code| known.include?(code.to_s) }
|
|
53
|
+
bad += pairs.reject { |pair| known_pair?(db, pair) }
|
|
54
|
+
codes & bad
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def pair_keyword?(code)
|
|
60
|
+
code.to_s.include?(PAIR_SEPARATOR)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def known_pair?(db, pair)
|
|
64
|
+
file, code, *rest = pair.split(PAIR_SEPARATOR, -1)
|
|
65
|
+
rest.empty? &&
|
|
66
|
+
known_keywords(db, dm.file_provenance_types_vocab).include?(file) &&
|
|
67
|
+
known_keywords(db, dm.code_provenance_types_vocab).include?(code)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# The concept codes of the vocabularies.
|
|
71
|
+
def known_keywords(db, vocabularies)
|
|
72
|
+
dm.concept_codes(db, vocabularies)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def all_provenance_types_vocab
|
|
76
|
+
[ dm.file_provenance_types_vocab, dm.code_provenance_types_vocab ].flatten
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def prov_of(db, vocabularies, ancestors)
|
|
80
|
+
dm.descendants_of(db, dm.concept_ids(db, vocabularies, ancestors))
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def plain_condition(db, codes)
|
|
17
84
|
Sequel.|(
|
|
18
|
-
{ file_provenance_type: prov_of(db,
|
|
85
|
+
{ file_provenance_type: prov_of(db, all_provenance_types_vocab, codes) },
|
|
86
|
+
{ code_provenance_type: prov_of(db, all_provenance_types_vocab, codes) }
|
|
19
87
|
)
|
|
20
88
|
end
|
|
21
89
|
|
|
22
|
-
|
|
23
|
-
|
|
90
|
+
# An invalid pair (validation reports it) still builds a condition. A
|
|
91
|
+
# half with no concept matches nothing, so the pair keeps no record.
|
|
92
|
+
def pair_condition(db, pair)
|
|
93
|
+
file, code = pair.split(PAIR_SEPARATOR, 2)
|
|
94
|
+
Sequel.&(
|
|
95
|
+
{ file_provenance_type: prov_of(db, dm.file_provenance_types_vocab, [ file ]) },
|
|
96
|
+
{ code_provenance_type: prov_of(db, dm.code_provenance_types_vocab, [ code ]) }
|
|
97
|
+
)
|
|
24
98
|
end
|
|
25
99
|
end
|
|
26
100
|
end
|
|
@@ -41,13 +41,17 @@ module ConceptQL
|
|
|
41
41
|
# With the Database's share_utilization option, the list is read from a
|
|
42
42
|
# table that every statement on the Database shares. The WITH entry only
|
|
43
43
|
# carries the list to Scope#with_ctes, which takes it out of the SQL and
|
|
44
|
-
# creates the table when the query first reads rows.
|
|
44
|
+
# creates the table when the query first reads rows. With
|
|
45
|
+
# CONCEPTQL_AVOID_CTES, with_ctes extracts nothing, so the scope keeps
|
|
46
|
+
# the table instead.
|
|
45
47
|
def evaluate(db)
|
|
46
48
|
cdb = nodifier.cdb
|
|
47
49
|
return super unless cdb.opts[:share_utilization]
|
|
48
50
|
|
|
49
51
|
list = select_it(query(db))
|
|
50
52
|
name = cdb.shared_utilization_table(list, scope)
|
|
53
|
+
return scope.add_shared_table(db, name, list) if ConceptQL.avoid_ctes?
|
|
54
|
+
|
|
51
55
|
db[name].with(name, list, Scope::SHARED_TABLE => true)
|
|
52
56
|
end
|
|
53
57
|
|
|
@@ -170,8 +174,6 @@ module ConceptQL
|
|
|
170
174
|
end
|
|
171
175
|
|
|
172
176
|
def override_columns
|
|
173
|
-
return unless gdm?
|
|
174
|
-
|
|
175
177
|
{
|
|
176
178
|
length_of_stay: (rdbms.days_between(Sequel[:admission_date],
|
|
177
179
|
discharge_date_column) + 1).as(:length_of_stay),
|
data/lib/conceptql/cli.rb
CHANGED
|
@@ -94,7 +94,7 @@ module ConceptQL
|
|
|
94
94
|
given by the connection options (or sequelizer's configuration).
|
|
95
95
|
DESC
|
|
96
96
|
option :counts, type: :boolean, default: false, desc: "annotate with per-domain row/person counts (needs a DB)"
|
|
97
|
-
option :data_model, desc: "data model: gdm, gdm_wide
|
|
97
|
+
option :data_model, desc: "data model: gdm, gdm_wide (default: $CONCEPTQL_DATA_MODEL or gdm)"
|
|
98
98
|
def render_json(statement_file)
|
|
99
99
|
cdb = if options[:counts]
|
|
100
100
|
ConceptQL::Database.new(db(options.except("counts", "data_model")),
|
|
@@ -10,6 +10,7 @@ module ConceptQL
|
|
|
10
10
|
def_delegators :nodifier, :cdb
|
|
11
11
|
def_delegators :cdb, :lexicon, :source_table_exists?
|
|
12
12
|
def_delegators :lexicon,
|
|
13
|
+
:code_meanings,
|
|
13
14
|
:concept_codes,
|
|
14
15
|
:concept_ids,
|
|
15
16
|
:concept_ids_by_name,
|
|
@@ -18,6 +19,7 @@ module ConceptQL
|
|
|
18
19
|
:concepts_by_name,
|
|
19
20
|
:concepts_ds,
|
|
20
21
|
:concepts_to_codes,
|
|
22
|
+
:concepts_to_codes_in_class,
|
|
21
23
|
:descendants_of,
|
|
22
24
|
:known_codes,
|
|
23
25
|
:related_concept_ids
|
|
@@ -47,79 +49,14 @@ module ConceptQL
|
|
|
47
49
|
db[table]
|
|
48
50
|
end
|
|
49
51
|
|
|
50
|
-
# The data columns whose concept ids the display-name joins look up, by
|
|
51
|
-
# table. A data model without those joins has none.
|
|
52
|
-
def name_concept_columns
|
|
53
|
-
{}
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def query_modifier_for(column)
|
|
57
|
-
{
|
|
58
|
-
visit_source_concept_id: ConceptQL::QueryModifiers::Omopv4Plus::PoSQueryModifier,
|
|
59
|
-
provider_id: ConceptQL::QueryModifiers::Omopv4Plus::ProviderQueryModifier,
|
|
60
|
-
context_specialty_concept_id: ConceptQL::QueryModifiers::Omopv4Plus::ProviderQueryModifier,
|
|
61
|
-
drug_name: ConceptQL::QueryModifiers::Omopv4Plus::DrugQueryModifier,
|
|
62
|
-
admission_date: ConceptQL::QueryModifiers::Omopv4Plus::AdmissionDateQueryModifier,
|
|
63
|
-
provenance_type: ConceptQL::QueryModifiers::Omopv4Plus::ProvenanceQueryModifier
|
|
64
|
-
}[column]
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def criterion_id
|
|
68
|
-
:criterion_id
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def table_id(table = nil)
|
|
72
|
-
return :person_id if table.to_sym == :death
|
|
73
|
-
|
|
74
|
-
:criterion_id if table.nil?
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def person_table
|
|
78
|
-
:person
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def make_table_id(table)
|
|
82
|
-
"#{table}_id".to_sym
|
|
83
|
-
end
|
|
84
|
-
|
|
85
52
|
def make_fk_id(table)
|
|
86
53
|
make_table_id(table)
|
|
87
54
|
end
|
|
88
55
|
|
|
89
|
-
def person_id(table)
|
|
90
|
-
case table.to_sym
|
|
91
|
-
when :procedure_cost, :drug_cost
|
|
92
|
-
nil
|
|
93
|
-
else
|
|
94
|
-
:person_id
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
|
|
98
56
|
def schema
|
|
99
57
|
SCHEMAS[data_model]
|
|
100
58
|
end
|
|
101
59
|
|
|
102
|
-
def data_model
|
|
103
|
-
:omopv4_plus
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def pk_by_domain(domain)
|
|
107
|
-
case domain.to_sym
|
|
108
|
-
when :death
|
|
109
|
-
:person_id
|
|
110
|
-
when :procedure_cost
|
|
111
|
-
:procedure_occurrence_id
|
|
112
|
-
when :drug_cost
|
|
113
|
-
:drug_exposure_id
|
|
114
|
-
else
|
|
115
|
-
"#{domain}_id".to_sym
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def fk_by_domain(domain)
|
|
120
|
-
"#{domain}_id".to_sym
|
|
121
|
-
end
|
|
122
|
-
|
|
123
60
|
def query_columns
|
|
124
61
|
@query_columns ||= Hash[operator.scope.query_columns.zip(operator.scope.query_columns)]
|
|
125
62
|
end
|
|
@@ -231,10 +168,6 @@ module ConceptQL
|
|
|
231
168
|
}
|
|
232
169
|
end
|
|
233
170
|
|
|
234
|
-
def table_to_domain(table)
|
|
235
|
-
table
|
|
236
|
-
end
|
|
237
|
-
|
|
238
171
|
def replace(replace_hash)
|
|
239
172
|
return {} unless replace_hash
|
|
240
173
|
|
|
@@ -276,14 +209,6 @@ module ConceptQL
|
|
|
276
209
|
end
|
|
277
210
|
end
|
|
278
211
|
|
|
279
|
-
def start_date_column(query, domain)
|
|
280
|
-
start_date_columns.merge(person: person_date_of_birth(query))[domain]
|
|
281
|
-
end
|
|
282
|
-
|
|
283
|
-
def end_date_column(query, domain)
|
|
284
|
-
end_date_columns.merge(person: person_date_of_birth(query))[domain]
|
|
285
|
-
end
|
|
286
|
-
|
|
287
212
|
def source_value_columns
|
|
288
213
|
@source_value_columns ||= assign_column_to_table do |table, columns|
|
|
289
214
|
reggy = /#{table.to_s.split('_').first}_source_value$/
|
|
@@ -292,81 +217,10 @@ module ConceptQL
|
|
|
292
217
|
end
|
|
293
218
|
end
|
|
294
219
|
|
|
295
|
-
def source_vocabulary_ids
|
|
296
|
-
@source_vocabulary_ids = assign_column_to_table do |table, columns|
|
|
297
|
-
next if %w[person death].any? { |tn| table.to_s =~ /#{tn}/ }
|
|
298
|
-
|
|
299
|
-
reggy = /#{table.to_s.split('_').first}_source_vocabulary_id/
|
|
300
|
-
column = columns.select { |k| k =~ reggy }.first
|
|
301
|
-
column || columns.select { |k| k =~ /_source_vocabulary_id/ }.first
|
|
302
|
-
end
|
|
303
|
-
end
|
|
304
|
-
|
|
305
|
-
def id_columns
|
|
306
|
-
@id_columns ||= assign_column_to_table do |table, columns|
|
|
307
|
-
reggy = /#{table}_id/
|
|
308
|
-
columns.select { |k| k =~ reggy }.first
|
|
309
|
-
end
|
|
310
|
-
end
|
|
311
|
-
|
|
312
|
-
def id_column(table)
|
|
313
|
-
id_columns[table]
|
|
314
|
-
end
|
|
315
|
-
|
|
316
|
-
def type_concept_id_columns
|
|
317
|
-
@type_concept_id_columns ||= assign_column_to_table do |_table, columns|
|
|
318
|
-
columns.select { |c| c =~ /_type_concept_id$/ }.first
|
|
319
|
-
end
|
|
320
|
-
end
|
|
321
|
-
|
|
322
|
-
def type_concept_id_column(domain)
|
|
323
|
-
type_concept_id_columns[table_by_domain(domain)]
|
|
324
|
-
end
|
|
325
|
-
|
|
326
|
-
def table_by_domain(domain)
|
|
327
|
-
domain
|
|
328
|
-
end
|
|
329
|
-
|
|
330
|
-
def period_table
|
|
331
|
-
:observation_period
|
|
332
|
-
end
|
|
333
|
-
|
|
334
220
|
def source_value_column(table)
|
|
335
221
|
source_value_columns[table]
|
|
336
222
|
end
|
|
337
223
|
|
|
338
|
-
def code_provenance_type_column(_query, domain)
|
|
339
|
-
{
|
|
340
|
-
condition_occurrence: :condition_type_concept_id,
|
|
341
|
-
death: :death_type_concept_id,
|
|
342
|
-
drug_exposure: :drug_type_concept_id,
|
|
343
|
-
observation: :observation_type_concept_id,
|
|
344
|
-
procedure_occurrence: :procedure_type_concept_id
|
|
345
|
-
}[domain]
|
|
346
|
-
end
|
|
347
|
-
|
|
348
|
-
def file_provenance_type_column(_query, domain)
|
|
349
|
-
{
|
|
350
|
-
condition_occurrence: :condition_type_concept_id,
|
|
351
|
-
death: :death_type_concept_id,
|
|
352
|
-
drug_exposure: :drug_type_concept_id,
|
|
353
|
-
observation: :observation_type_concept_id,
|
|
354
|
-
procedure_occurrence: :procedure_type_concept_id
|
|
355
|
-
}[domain]
|
|
356
|
-
end
|
|
357
|
-
|
|
358
|
-
def provider_id_column(_query, domain)
|
|
359
|
-
{
|
|
360
|
-
condition_occurrence: :associated_provider_id,
|
|
361
|
-
death: :death_type_concept_id,
|
|
362
|
-
drug_exposure: :prescribing_provider_id,
|
|
363
|
-
observation: :associated_provider_id,
|
|
364
|
-
person: :provider_id,
|
|
365
|
-
procedure_occurrence: :associated_provider_id,
|
|
366
|
-
provider: :provider_id
|
|
367
|
-
}[domain]
|
|
368
|
-
end
|
|
369
|
-
|
|
370
224
|
def place_of_service_concept_id_column(_query, domain)
|
|
371
225
|
return nil if domain.nil?
|
|
372
226
|
return Sequel.cast(:visit_source_concept_id, :Bigint) if table_cols(domain).include?(pos_table_fk)
|
|
@@ -374,15 +228,6 @@ module ConceptQL
|
|
|
374
228
|
nil
|
|
375
229
|
end
|
|
376
230
|
|
|
377
|
-
def pos_table_fk
|
|
378
|
-
:visit_occurrence_id
|
|
379
|
-
end
|
|
380
|
-
|
|
381
|
-
def person_date_of_birth(_query)
|
|
382
|
-
rdbms.cast_date(ConceptQL::Utils.assemble_date(:year_of_birth, :month_of_birth, :day_of_birth,
|
|
383
|
-
database_type: nodifier.database_type))
|
|
384
|
-
end
|
|
385
|
-
|
|
386
231
|
def date_columns(query, table = nil)
|
|
387
232
|
sd = start_date_column(query, table)
|
|
388
233
|
sd = rdbms.cast_date(Sequel.expr(sd)).as(:start_date) unless sd == :start_date
|
|
@@ -414,10 +259,7 @@ module ConceptQL
|
|
|
414
259
|
end
|
|
415
260
|
|
|
416
261
|
def table_columns(*tables)
|
|
417
|
-
tables.
|
|
418
|
-
p o
|
|
419
|
-
p tables
|
|
420
|
-
end
|
|
262
|
+
tables.flat_map { |t| table_cols(t) }
|
|
421
263
|
end
|
|
422
264
|
|
|
423
265
|
def table_source_value(table)
|
|
@@ -428,10 +270,6 @@ module ConceptQL
|
|
|
428
270
|
source_vocabulary_ids[table_to_sym(table)]
|
|
429
271
|
end
|
|
430
272
|
|
|
431
|
-
def table_is_missing?(db)
|
|
432
|
-
!(source_table_exists?(db, :observation_period) && source_table_exists?(db, :condition_occurrence))
|
|
433
|
-
end
|
|
434
|
-
|
|
435
273
|
def concept_id(table)
|
|
436
274
|
table_cols(table).find do |col|
|
|
437
275
|
col = col.to_s
|
|
@@ -451,12 +289,6 @@ module ConceptQL
|
|
|
451
289
|
end
|
|
452
290
|
end
|
|
453
291
|
end
|
|
454
|
-
|
|
455
|
-
def information_period_where_clause(arguments)
|
|
456
|
-
return if arguments.empty?
|
|
457
|
-
|
|
458
|
-
{ plan_source_value: arguments }
|
|
459
|
-
end
|
|
460
292
|
end
|
|
461
293
|
end
|
|
462
294
|
end
|
|
@@ -115,8 +115,10 @@ module ConceptQL
|
|
|
115
115
|
:context_id
|
|
116
116
|
end
|
|
117
117
|
|
|
118
|
+
# The schema of a data model does not change, so the answer is kept, as
|
|
119
|
+
# for start_date_columns and the other column lookups.
|
|
118
120
|
def source_vocabulary_ids
|
|
119
|
-
@source_vocabulary_ids
|
|
121
|
+
@source_vocabulary_ids ||= assign_column_to_table do |table, columns|
|
|
120
122
|
next if %w[patients death].any? { |tn| table.to_s =~ /#{tn}/ }
|
|
121
123
|
|
|
122
124
|
reggy = /#{table.to_s.split('_').first}_vocabulary_id/
|
data/lib/conceptql/data_model.rb
CHANGED
|
@@ -2,11 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "data_model/gdm"
|
|
4
4
|
require_relative "data_model/gdm_wide"
|
|
5
|
-
require_relative "data_model/omopv4_plus"
|
|
6
5
|
require_relative "rdbms"
|
|
7
6
|
|
|
8
7
|
module ConceptQL
|
|
9
8
|
module DataModel
|
|
9
|
+
SUPPORTED = %i[gdm gdm_wide].freeze
|
|
10
|
+
|
|
11
|
+
# Raised when a caller asks for a data model that ConceptQL does not have.
|
|
12
|
+
class UnsupportedError < ArgumentError
|
|
13
|
+
REMOVED = { omopv4_plus: "0.5.0" }.freeze
|
|
14
|
+
|
|
15
|
+
def initialize(data_model)
|
|
16
|
+
message = "ConceptQL has no data model #{data_model.inspect}. Use one of: #{SUPPORTED.join(', ')}."
|
|
17
|
+
if (version = REMOVED[data_model])
|
|
18
|
+
message = "The #{data_model} data model was removed in ConceptQL #{version}. Use one of: #{SUPPORTED.join(', ')}."
|
|
19
|
+
end
|
|
20
|
+
super(message)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.assert_supported!(data_model)
|
|
25
|
+
raise UnsupportedError, data_model unless SUPPORTED.include?(data_model)
|
|
26
|
+
end
|
|
27
|
+
|
|
10
28
|
def self.get(data_model, opts = {})
|
|
11
29
|
operator = opts[:operator]
|
|
12
30
|
nodifier = opts[:nodifier]
|
|
@@ -16,10 +34,8 @@ module ConceptQL
|
|
|
16
34
|
Gdm.new(operator, nodifier)
|
|
17
35
|
when :gdm_wide
|
|
18
36
|
GdmWide.new(operator, nodifier)
|
|
19
|
-
when :omopv4_plus
|
|
20
|
-
Omopv4Plus.new(operator, nodifier)
|
|
21
37
|
else
|
|
22
|
-
raise
|
|
38
|
+
raise UnsupportedError, data_model
|
|
23
39
|
end
|
|
24
40
|
end
|
|
25
41
|
|
data/lib/conceptql/database.rb
CHANGED
|
@@ -28,6 +28,7 @@ module ConceptQL
|
|
|
28
28
|
@opts = ConceptQL::Utils.rekey(opts, rekey_values: true)
|
|
29
29
|
|
|
30
30
|
@opts[:data_model] ||= (ENV["CONCEPTQL_DATA_MODEL"] || ConceptQL::DEFAULT_DATA_MODEL).to_sym
|
|
31
|
+
ConceptQL::DataModel.assert_supported!(@opts[:data_model])
|
|
31
32
|
|
|
32
33
|
self.class.db_extensions(db, @opts[:data_model], @opts[:use_cold_col])
|
|
33
34
|
db_type = db ? db.database_type.to_sym : :postgres
|
|
@@ -94,8 +95,10 @@ module ConceptQL
|
|
|
94
95
|
end
|
|
95
96
|
|
|
96
97
|
# The LEXICON_URL database, or nil when it is unset. Lexicon tries the
|
|
97
|
-
# data db first
|
|
98
|
-
#
|
|
98
|
+
# data db first. When neither has the vocabulary tables, a real data db
|
|
99
|
+
# raises Lexicon::NoVocabularyError, and a db-less Database (db nil or a
|
|
100
|
+
# Sequel mock) falls back to LexiconNoDB, so no database (and no sqlite3
|
|
101
|
+
# gem) is needed here.
|
|
99
102
|
def make_lexicon_db
|
|
100
103
|
return unless ENV["LEXICON_URL"]
|
|
101
104
|
|
|
@@ -125,6 +128,9 @@ module ConceptQL
|
|
|
125
128
|
@source_tables.fetch(key) { db.table_exists?(table) && (@source_tables[key] = true) }
|
|
126
129
|
end
|
|
127
130
|
|
|
131
|
+
# The Lexicon passed as lexicon:, or one built from the data db and the
|
|
132
|
+
# LEXICON_URL database. Database.new calls it, so a real data db without
|
|
133
|
+
# vocabulary tables raises Lexicon::NoVocabularyError there.
|
|
128
134
|
def lexicon
|
|
129
135
|
@lexicon ||= Lexicon.new(self.class.lexicon_db, db, cache: @opts[:cache_lookups])
|
|
130
136
|
end
|
|
@@ -139,6 +145,7 @@ module ConceptQL
|
|
|
139
145
|
# differs from this Database's.
|
|
140
146
|
def operators(data_model = opts[:data_model])
|
|
141
147
|
data_model = data_model.to_sym
|
|
148
|
+
ConceptQL::DataModel.assert_supported!(data_model)
|
|
142
149
|
@operators ||= {}
|
|
143
150
|
@operators[data_model] ||= begin
|
|
144
151
|
vocabulary_operators = Vocabularies::DynamicVocabularies.new(lexicon).operators(data_model)
|
|
@@ -26,8 +26,7 @@ module ConceptQL
|
|
|
26
26
|
|
|
27
27
|
origin_column = column
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
while (chr = chrs.shift) =~ /[res]/i
|
|
29
|
+
prefix.each_char do |chr|
|
|
31
30
|
if chr.downcase == "r"
|
|
32
31
|
reverse = true
|
|
33
32
|
else
|
|
@@ -53,13 +52,20 @@ module ConceptQL
|
|
|
53
52
|
op.rdbms.cast_date(adjusted_date)
|
|
54
53
|
end
|
|
55
54
|
|
|
56
|
-
# Returns an array of
|
|
55
|
+
# Returns an array of [units, quantity] pairs that represent date modifiers.
|
|
56
|
+
# An "r" in the prefix reverses them, as it does in #adjust.
|
|
57
57
|
def adjustments(reverse = false)
|
|
58
|
-
|
|
58
|
+
reverse ||= prefix.match?(/r/i)
|
|
59
|
+
(@adjustments ||= {})[reverse] ||= parse(str.delete_prefix(prefix), reverse)
|
|
59
60
|
end
|
|
60
61
|
|
|
61
62
|
private
|
|
62
63
|
|
|
64
|
+
# The leading e/r/s characters, which pick the origin column and the direction
|
|
65
|
+
def prefix
|
|
66
|
+
str[/\A[res]*/i]
|
|
67
|
+
end
|
|
68
|
+
|
|
63
69
|
def lookup
|
|
64
70
|
{
|
|
65
71
|
"y" => :years,
|
|
@@ -71,7 +77,8 @@ module ConceptQL
|
|
|
71
77
|
|
|
72
78
|
def parse(str, reverse)
|
|
73
79
|
return [] if str.nil? || str.empty?
|
|
74
|
-
|
|
80
|
+
# A bare number means days
|
|
81
|
+
return [ [ lookup["d"], reverse ? -str.to_i : str.to_i ] ] if str.match?(/\A[-+]?\d+\z/)
|
|
75
82
|
|
|
76
83
|
str.downcase.scan(/([-+]?\d*[dwmy])/).map do |adjustment|
|
|
77
84
|
adjustment = adjustment.first
|
|
@@ -4,9 +4,12 @@ require_relative "lexicon_strategy"
|
|
|
4
4
|
|
|
5
5
|
module ConceptQL
|
|
6
6
|
class LexiconGDM < LexiconStrategy
|
|
7
|
+
# The GDM vocabulary tables. A db that has all four can be the lexicon.
|
|
8
|
+
VOCABULARY_TABLES = %i[ancestors concepts mappings vocabularies].freeze
|
|
9
|
+
|
|
7
10
|
class << self
|
|
8
11
|
def db_has_all_vocabulary_tables?(db)
|
|
9
|
-
|
|
12
|
+
VOCABULARY_TABLES.all? { |t| db.table_exists?(t) }
|
|
10
13
|
end
|
|
11
14
|
end
|
|
12
15
|
|
|
@@ -4,9 +4,12 @@ require_relative "lexicon_strategy"
|
|
|
4
4
|
|
|
5
5
|
module ConceptQL
|
|
6
6
|
class LexiconOhdsi < LexiconStrategy
|
|
7
|
+
# The OHDSI vocabulary tables. A db that has all four can be the lexicon.
|
|
8
|
+
VOCABULARY_TABLES = %i[concept concept_ancestor concept_relationship vocabulary].freeze
|
|
9
|
+
|
|
7
10
|
class << self
|
|
8
11
|
def db_has_all_vocabulary_tables?(db)
|
|
9
|
-
|
|
12
|
+
VOCABULARY_TABLES.all? { |t| db.table_exists?(t) }
|
|
10
13
|
end
|
|
11
14
|
end
|
|
12
15
|
|
|
@@ -56,6 +59,21 @@ module ConceptQL
|
|
|
56
59
|
!db.table_exists?(:concept)
|
|
57
60
|
end
|
|
58
61
|
|
|
62
|
+
# A caller can answer "is the concept table missing?" with a block. A
|
|
63
|
+
# concept table without concept_class_id answers nil, as a lexicon
|
|
64
|
+
# without classes does.
|
|
65
|
+
def concepts_to_codes_in_class(data_db, vocabulary_id, concept_class_id, codes)
|
|
66
|
+
return nil if codes.blank? || db_is_mock?(data_db)
|
|
67
|
+
return nil if block_given? ? yield : table_is_missing?(data_db)
|
|
68
|
+
|
|
69
|
+
db[:concept]
|
|
70
|
+
.where(vocabulary_id: vocabulary_id, concept_class_id: concept_class_id)
|
|
71
|
+
.where(code_condition(vocabulary_id, codes))
|
|
72
|
+
.select_map(%i[concept_code concept_name])
|
|
73
|
+
rescue Sequel::DatabaseError
|
|
74
|
+
nil
|
|
75
|
+
end
|
|
76
|
+
|
|
59
77
|
def strategy
|
|
60
78
|
:ohdsi
|
|
61
79
|
end
|