conceptql 0.7.0 → 0.8.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 +155 -0
- data/README.md +4 -0
- data/config/vocabularies.csv +1 -0
- data/docs/spec/README.md +5 -3
- data/docs/spec/examples/recall-operator.json +6 -3
- data/lib/conceptql/annotation.rb +136 -0
- data/lib/conceptql/behaviors/provenanceable.rb +44 -26
- data/lib/conceptql/cli.rb +0 -6
- data/lib/conceptql/database.rb +74 -4
- data/lib/conceptql/diagram.rb +4 -1
- data/lib/conceptql/diagram_renderer/geometry.rb +6 -1
- data/lib/conceptql/lexicon/lexicon_strategy.rb +23 -12
- data/lib/conceptql/lexicon.rb +24 -35
- data/lib/conceptql/nodifier.rb +16 -0
- data/lib/conceptql/operators/co_reported.rb +1 -0
- data/lib/conceptql/operators/information_periods.rb +7 -3
- data/lib/conceptql/operators/multiple_vocabularies.rb +42 -22
- data/lib/conceptql/operators/operator.rb +38 -8
- data/lib/conceptql/operators/place_of_service_filter.rb +24 -12
- data/lib/conceptql/operators/provenance.rb +11 -11
- data/lib/conceptql/operators/recall.rb +5 -0
- data/lib/conceptql/operators/union.rb +13 -2
- data/lib/conceptql/operators/vocabulary.rb +73 -24
- data/lib/conceptql/query.rb +111 -37
- data/lib/conceptql/scope.rb +1 -18
- data/lib/conceptql/version.rb +1 -1
- data/lib/conceptql/vocabularies/drg_systems.rb +3 -3
- data/lib/conceptql/vocabularies/dynamic_vocabularies.rb +15 -10
- data/lib/conceptql/vocabularies/entry.rb +2 -2
- data/lib/conceptql/vocabularies/read_codes.rb +42 -0
- data/lib/conceptql/vocabularies/vocabulary_class_factory.rb +3 -0
- metadata +3 -3
- data/lib/conceptql/lexicon/lexicon_gdm.rb +0 -50
- data/lib/conceptql/operators/read.rb +0 -79
|
@@ -6,12 +6,13 @@ module ConceptQL
|
|
|
6
6
|
module Operators
|
|
7
7
|
class Vocabulary < Operator
|
|
8
8
|
# The vocabulary operators share one reference section (spec_id
|
|
9
|
-
# vocabulary-operator): each generated vocabulary operator
|
|
10
|
-
# the multiple vocabulary operators. So the section's texts
|
|
11
|
-
# on this class: `returns`, the meaning: of the codes
|
|
12
|
-
# `example`. An operator of the section extends this
|
|
13
|
-
# the returns text, and only the examples whose
|
|
14
|
-
# its own name, so no operator shows an example
|
|
9
|
+
# vocabulary-operator): each generated vocabulary operator (read is
|
|
10
|
+
# one) and the multiple vocabulary operators. So the section's texts
|
|
11
|
+
# are written on this class: `returns`, the meaning: of the codes
|
|
12
|
+
# argument, and each `example`. An operator of the section extends this
|
|
13
|
+
# module and serves the returns text, and only the examples whose
|
|
14
|
+
# statement starts with its own name, so no operator shows an example
|
|
15
|
+
# of another one.
|
|
15
16
|
module SharedDocumentation
|
|
16
17
|
def get_returns
|
|
17
18
|
super || Vocabulary.returns
|
|
@@ -79,32 +80,46 @@ module ConceptQL
|
|
|
79
80
|
"HCPCS" => { vocabulary_id: "CPT4_HCPCS", operator: "cpt_or_hcpcs" }
|
|
80
81
|
}.freeze
|
|
81
82
|
NO_RECORDS_IN_DATA = "the data has no records of this vocabulary"
|
|
83
|
+
NOT_PRESENT_IN_DATASET = "code(s) not present in this dataset"
|
|
82
84
|
|
|
83
85
|
# MultipleVocabularies passes skip_merged_vocabulary_check to its
|
|
84
86
|
# vocabulary operators. It reads the merged vocabulary itself, and it
|
|
85
|
-
# would drop
|
|
87
|
+
# would drop these warnings anyway (see #warn_about_merged_vocabulary).
|
|
88
|
+
# The merged-vocabulary and the no-records checks read the data, so
|
|
89
|
+
# they need a data database. The code checks read only the lexicon.
|
|
90
|
+
# When the merged-vocabulary warning fires, it names the operator to
|
|
91
|
+
# use, so the no-records check does not add a second warning with the
|
|
92
|
+
# same text.
|
|
93
|
+
#
|
|
94
|
+
# Database#check_codes sorts the codes that the Database's lexicon does
|
|
95
|
+
# not know into "code(s) not present in this dataset" and "unknown
|
|
96
|
+
# code(s)", and applies the 2020 rule. Each warning lists every code.
|
|
97
|
+
# When the data has no records of the vocabulary at all, the no-records
|
|
98
|
+
# warning already says that every code is absent, so the not-present
|
|
99
|
+
# warning is not given. The unknown-code and reused-code warnings still
|
|
100
|
+
# are, so a typo is not hidden (bead conceptql-dufi).
|
|
86
101
|
def additional_validation(db, opts = {})
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
102
|
+
if add_warnings?(db, opts) && !opts[:skip_merged_vocabulary_check]
|
|
103
|
+
merged_warning = warn_about_merged_vocabulary(db)
|
|
104
|
+
check_for_records(db, warn: !merged_warning)
|
|
105
|
+
end
|
|
106
|
+
return unless lexicon_checks?(opts)
|
|
90
107
|
return if select_all?
|
|
91
108
|
|
|
92
|
-
args = arguments
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
# so its lookups do not raise CodeCaseMismatchError (bead conceptql-rz8s).
|
|
97
|
-
missing_args = args - dm.known_codes(db, vocabulary_id, args, check_case: false)
|
|
98
|
-
add_warning(UNKNOWN_CODE, *missing_args) unless missing_args.empty?
|
|
109
|
+
args = arguments - bad_arguments
|
|
110
|
+
not_present, unknown = nodifier.cdb.check_codes(db, vocabulary_id, args)
|
|
111
|
+
add_warning(NOT_PRESENT_IN_DATASET, *not_present) unless not_present.empty? || @no_records_in_data
|
|
112
|
+
add_warning(UNKNOWN_CODE, *unknown) unless unknown.empty?
|
|
99
113
|
|
|
100
|
-
warn_about_reused_codes(db, args -
|
|
114
|
+
warn_about_reused_codes(db, args - not_present - unknown)
|
|
101
115
|
end
|
|
102
116
|
|
|
103
117
|
# Warns when the data has no record of this vocabulary but has records
|
|
104
118
|
# of the merged vocabulary that holds its codes, and the Database's
|
|
105
119
|
# lexicon carries that merged vocabulary (bead conceptql-k5ho). The
|
|
106
120
|
# warning names the operator to use instead. The test fixture keeps
|
|
107
|
-
# CPT4 and HCPCS rows, so it never shows this warning.
|
|
121
|
+
# CPT4 and HCPCS rows, so it never shows this warning. It returns the
|
|
122
|
+
# warnings when it adds one, and nil otherwise.
|
|
108
123
|
def warn_about_merged_vocabulary(db)
|
|
109
124
|
merged = MERGED_VOCABULARIES[vocabulary_id]
|
|
110
125
|
return unless merged && nodifier.operators.key?(merged[:vocabulary_id].downcase)
|
|
@@ -117,6 +132,40 @@ module ConceptQL
|
|
|
117
132
|
"the data stores #{vocabulary_id} codes as #{merged[:vocabulary_id]}, so use #{merged[:operator]}")
|
|
118
133
|
end
|
|
119
134
|
|
|
135
|
+
# Warns when etl_vocabulary_ids lists the vocabularies of the data's
|
|
136
|
+
# clinical codes and this vocabulary is not among them (bead
|
|
137
|
+
# conceptql-d1bd). A count of 0 would then say that no patient has the
|
|
138
|
+
# codes, which the data cannot show, so #annotate reports null counts.
|
|
139
|
+
# With warn: false (the merged-vocabulary warning fired, with the same
|
|
140
|
+
# text), the counts are null and no second warning is added.
|
|
141
|
+
# etl_vocabulary_ids covers the clinical codes only, so an operator
|
|
142
|
+
# that reads another table (death type, observation period type) is
|
|
143
|
+
# not checked. Neither is a dataset without the table.
|
|
144
|
+
def check_for_records(db, warn: true)
|
|
145
|
+
return unless absent_from_data?(db)
|
|
146
|
+
|
|
147
|
+
@no_records_in_data = true
|
|
148
|
+
add_warning(NO_RECORDS_IN_DATA) if warn
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Whether etl_vocabulary_ids lists the vocabularies of the data's
|
|
152
|
+
# clinical codes and this vocabulary is not among them. See
|
|
153
|
+
# #check_for_records. MultipleVocabularies asks each of its vocabulary
|
|
154
|
+
# operators.
|
|
155
|
+
def absent_from_data?(db)
|
|
156
|
+
return false unless criterion_table == dm.table_by_domain(:clinical_codes)
|
|
157
|
+
return false unless (ids = nodifier.etl_vocabulary_ids(db))
|
|
158
|
+
|
|
159
|
+
!ids.include?(vocabulary_id)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Whether the data has no records of this vocabulary at all, as
|
|
163
|
+
# #check_for_records found.
|
|
164
|
+
def no_records_in_data?(db, opts = {})
|
|
165
|
+
validate(db, opts)
|
|
166
|
+
@no_records_in_data || false
|
|
167
|
+
end
|
|
168
|
+
|
|
120
169
|
# A code that the vocabulary reused for a new meaning selects records of
|
|
121
170
|
# both meanings: DRG 410 selects chemotherapy stays before 2007-10-01 and
|
|
122
171
|
# biliary tract procedures after it (conceptql-ttk). Each item names the
|
|
@@ -125,7 +174,7 @@ module ConceptQL
|
|
|
125
174
|
def warn_about_reused_codes(db, codes)
|
|
126
175
|
meanings = dm.code_meanings(db, vocabulary_id, codes, check_case: false)
|
|
127
176
|
reused = codes.filter_map do |code|
|
|
128
|
-
names = meanings[code
|
|
177
|
+
names = meanings[code]
|
|
129
178
|
"#{code}: #{names.join(' | ')}" if names
|
|
130
179
|
end
|
|
131
180
|
add_warning("code(s) with more than one meaning", *reused) unless reused.empty?
|
|
@@ -157,6 +206,10 @@ module ConceptQL
|
|
|
157
206
|
{ clinical_code_vocabulary_id: vocabulary_id }
|
|
158
207
|
end
|
|
159
208
|
|
|
209
|
+
def vocabulary_id
|
|
210
|
+
@vocabulary_id ||= translated_vocabulary_id
|
|
211
|
+
end
|
|
212
|
+
|
|
160
213
|
private
|
|
161
214
|
|
|
162
215
|
def query(db)
|
|
@@ -222,10 +275,6 @@ module ConceptQL
|
|
|
222
275
|
vocab_entry.omopv5_id == "NDC" ? NDC_BAD_FORMAT : BAD_FORMAT
|
|
223
276
|
end
|
|
224
277
|
|
|
225
|
-
def vocabulary_id
|
|
226
|
-
@vocabulary_id ||= translated_vocabulary_id
|
|
227
|
-
end
|
|
228
|
-
|
|
229
278
|
def translated_vocabulary_id
|
|
230
279
|
vocab_entry.omopv5_vocabulary_id || op_name
|
|
231
280
|
end
|
data/lib/conceptql/query.rb
CHANGED
|
@@ -5,6 +5,7 @@ require "forwardable"
|
|
|
5
5
|
require_relative "scope"
|
|
6
6
|
require_relative "nodifier"
|
|
7
7
|
require_relative "one_query_counts"
|
|
8
|
+
require_relative "annotation"
|
|
8
9
|
require "sequel/sqlformat"
|
|
9
10
|
|
|
10
11
|
module ConceptQL
|
|
@@ -29,9 +30,10 @@ module ConceptQL
|
|
|
29
30
|
# errors. An invalid statement can crash while it builds, or build SQL that
|
|
30
31
|
# silently returns fewer rows. Warnings do not raise.
|
|
31
32
|
#
|
|
32
|
-
# #errors is the
|
|
33
|
-
#
|
|
34
|
-
#
|
|
33
|
+
# #errors is the list of every error that annotate shows, in the form of
|
|
34
|
+
# ConceptQL::Annotation#errors ({ operator:, id:, message:, details: }), so
|
|
35
|
+
# two operators with the same name keep their own errors. #statement is the
|
|
36
|
+
# statement that the Query holds.
|
|
35
37
|
class InvalidStatementError < Query::QueryError
|
|
36
38
|
attr_reader :statement, :errors
|
|
37
39
|
|
|
@@ -39,15 +41,15 @@ module ConceptQL
|
|
|
39
41
|
@statement = statement
|
|
40
42
|
@errors = errors
|
|
41
43
|
super([ "Invalid ConceptQL statement. Validation errors:",
|
|
42
|
-
*errors.
|
|
44
|
+
*errors.map { |error| " #{error[:operator]}: #{describe(error)}" },
|
|
43
45
|
"Statement: #{statement.inspect}" ].join("\n"))
|
|
44
46
|
end
|
|
45
47
|
|
|
46
48
|
private
|
|
47
49
|
|
|
48
50
|
def describe(error)
|
|
49
|
-
|
|
50
|
-
details.empty? ? message.to_s : "#{message} (#{details.flatten.map(&:to_s).join(', ')})"
|
|
51
|
+
details = error[:details]
|
|
52
|
+
details.empty? ? error[:message].to_s : "#{error[:message]} (#{details.flatten.map(&:to_s).join(', ')})"
|
|
51
53
|
end
|
|
52
54
|
end
|
|
53
55
|
|
|
@@ -63,6 +65,7 @@ module ConceptQL
|
|
|
63
65
|
|
|
64
66
|
def initialize(cdb, statement, opts = {})
|
|
65
67
|
@cdb = cdb
|
|
68
|
+
@given_statement = statement
|
|
66
69
|
@statement, opts = extract_statement(statement, opts)
|
|
67
70
|
@opts = opts
|
|
68
71
|
opts[:algorithm_fetcher] ||= proc do |alg|
|
|
@@ -131,21 +134,43 @@ module ConceptQL
|
|
|
131
134
|
raise QueryError, "Failed to generate SQL for #{statement.inspect}"
|
|
132
135
|
end
|
|
133
136
|
|
|
134
|
-
#
|
|
135
|
-
#
|
|
136
|
-
#
|
|
137
|
+
# The statement, with an annotation hash in each operator: its effective
|
|
138
|
+
# id (see Annotation.assign_ids), its counts, errors and warnings. With
|
|
139
|
+
# counts on, DuckDB and Postgres count every operator with one SQL
|
|
140
|
+
# statement (OneQueryCounts). ConceptQL::Annotation reads the result.
|
|
137
141
|
#
|
|
138
|
-
# The
|
|
139
|
-
#
|
|
140
|
-
#
|
|
142
|
+
# The Query keeps one result for each opts hash, so a repeated call with
|
|
143
|
+
# the same opts annotates once. Operators memoize #annotate and write into
|
|
144
|
+
# their own options, so each opts hash gets a fresh operator tree, and a
|
|
145
|
+
# call with other opts gets its own correct result.
|
|
146
|
+
#
|
|
147
|
+
# A window statement keeps its window operator at the top, and a window
|
|
148
|
+
# table that is a ConceptQL statement is annotated in place, in the
|
|
149
|
+
# window_table option of the window operator, so each of its errors stays
|
|
150
|
+
# on its own operator. When the window table has errors, the outer
|
|
151
|
+
# statement is not counted, because every count joins the window table,
|
|
152
|
+
# and building its SQL raises.
|
|
141
153
|
def annotate(opts = {})
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
154
|
+
annotated_tree(opts).annotated
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# The errors of the statement, as ConceptQL::Annotation#errors gives them.
|
|
158
|
+
# They come from the kept annotation with skip_counts: true, so no count
|
|
159
|
+
# SQL runs.
|
|
160
|
+
def errors
|
|
161
|
+
annotation.errors
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# The warnings of the statement, as ConceptQL::Annotation#warnings gives
|
|
165
|
+
# them, from the same kept annotation as #errors.
|
|
166
|
+
def warnings
|
|
167
|
+
annotation.warnings
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# True when no operator has an error. Warnings do not make a statement
|
|
171
|
+
# invalid.
|
|
172
|
+
def valid?
|
|
173
|
+
annotation.valid?
|
|
149
174
|
end
|
|
150
175
|
|
|
151
176
|
def accept(visitor, _opts = {})
|
|
@@ -158,11 +183,6 @@ module ConceptQL
|
|
|
158
183
|
visitor.results
|
|
159
184
|
end
|
|
160
185
|
|
|
161
|
-
def scope_annotate(opts = {})
|
|
162
|
-
annotate(opts)
|
|
163
|
-
nodifier.scope.annotation
|
|
164
|
-
end
|
|
165
|
-
|
|
166
186
|
def domains
|
|
167
187
|
operator.domains(db)
|
|
168
188
|
end
|
|
@@ -200,21 +220,55 @@ module ConceptQL
|
|
|
200
220
|
operator.code_list(db).uniq
|
|
201
221
|
end
|
|
202
222
|
|
|
203
|
-
#
|
|
204
|
-
#
|
|
205
|
-
#
|
|
206
|
-
#
|
|
223
|
+
# Every error of the statement, the check that callers use to decide
|
|
224
|
+
# whether a statement is valid. It is #errors: it reads the kept
|
|
225
|
+
# annotation with skip_counts: true, so it never runs count SQL. #sql
|
|
226
|
+
# calls it.
|
|
207
227
|
def validation_errors
|
|
208
|
-
|
|
228
|
+
errors
|
|
209
229
|
end
|
|
210
230
|
|
|
211
231
|
def drop_temp_tables(opts = {})
|
|
212
232
|
query.drop_temp_tables(opts)
|
|
213
233
|
end
|
|
214
234
|
|
|
235
|
+
protected
|
|
236
|
+
|
|
237
|
+
attr_reader :nodifier, :annotated
|
|
238
|
+
|
|
239
|
+
# Annotates this Query's own operator tree once. Only #annotated_tree
|
|
240
|
+
# calls it, on a fresh Query.
|
|
241
|
+
def annotate_own_tree(opts)
|
|
242
|
+
@annotated = Annotation.assign_ids(annotate_without_ids(opts))
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# The annotated statement before Annotation.assign_ids, so that the ids
|
|
246
|
+
# of a window table statement are assigned with the ids of the statement
|
|
247
|
+
# around it. It annotates this Query's own operator tree, so only a fresh
|
|
248
|
+
# Query calls it.
|
|
249
|
+
def annotate_without_ids(opts)
|
|
250
|
+
opts = opts.merge(skip_counts: true) unless window_table_errors.empty?
|
|
251
|
+
nodifier.scope.one_query_counts = OneQueryCounts.new(operator, db, opts)
|
|
252
|
+
with_window(operator.annotate(db, opts), opts)
|
|
253
|
+
end
|
|
254
|
+
|
|
215
255
|
private
|
|
216
256
|
|
|
217
|
-
attr_reader :cdb
|
|
257
|
+
attr_reader :cdb
|
|
258
|
+
|
|
259
|
+
# A fresh Query of the given statement (with its window operator) for
|
|
260
|
+
# each opts hash, with its own operator tree and scope, that has annotated
|
|
261
|
+
# its tree with those opts. This Query's own tree is never annotated, so
|
|
262
|
+
# #query and #sql do not depend on annotate calls.
|
|
263
|
+
def annotated_tree(opts)
|
|
264
|
+
(@annotated_trees ||= {})[opts.dup] ||= Query.new(cdb, @given_statement, @opts).tap do |tree|
|
|
265
|
+
tree.annotate_own_tree(opts)
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def annotation
|
|
270
|
+
Annotation.new(annotate(skip_counts: true))
|
|
271
|
+
end
|
|
218
272
|
|
|
219
273
|
# Raises InvalidStatementError if #validation_errors is not empty. The
|
|
220
274
|
# statement and the opts of a Query do not change, so it checks once.
|
|
@@ -229,20 +283,39 @@ module ConceptQL
|
|
|
229
283
|
@valid = true
|
|
230
284
|
end
|
|
231
285
|
|
|
286
|
+
# The window table of the scope, as Window::Table reads it: a table name
|
|
287
|
+
# or a ConceptQL statement (an Array)
|
|
288
|
+
def window_table
|
|
289
|
+
scope_opts = nodifier.scope.opts
|
|
290
|
+
scope_opts.merge(scope_opts[:window_opts] || {})[:window_table]
|
|
291
|
+
end
|
|
292
|
+
|
|
232
293
|
# The validation errors of the window table's statement, when the window
|
|
233
|
-
# table is a ConceptQL statement
|
|
234
|
-
#
|
|
235
|
-
#
|
|
294
|
+
# table is a ConceptQL statement, as one list of [message, *details]. Each
|
|
295
|
+
# message starts with the name of the operator that reported it. A table
|
|
296
|
+
# name has no errors.
|
|
236
297
|
def window_table_errors
|
|
237
|
-
scope_opts = nodifier.scope.opts
|
|
238
|
-
window_table = scope_opts.merge(scope_opts[:window_opts] || {})[:window_table]
|
|
239
298
|
return [] unless window_table.is_a?(Array)
|
|
240
299
|
|
|
241
|
-
cdb.query(window_table).validation_errors.
|
|
242
|
-
|
|
300
|
+
cdb.query(window_table).validation_errors.map do |error|
|
|
301
|
+
[ "#{error[:operator]}: #{error[:message]}", *error[:details] ]
|
|
243
302
|
end
|
|
244
303
|
end
|
|
245
304
|
|
|
305
|
+
# The annotated statement inside its window operator, when the statement
|
|
306
|
+
# is a window statement or the scope's window table is a statement. The
|
|
307
|
+
# window operator has the options of the statement's window and an
|
|
308
|
+
# annotation hash, which holds only its id. A window table statement is
|
|
309
|
+
# annotated in place, with the same opts.
|
|
310
|
+
def with_window(annotated, opts)
|
|
311
|
+
table = window_table
|
|
312
|
+
return annotated unless @window_options || table.is_a?(Array)
|
|
313
|
+
|
|
314
|
+
options = (@window_options || {}).dup
|
|
315
|
+
options[:window_table] = cdb.query(table).annotate_without_ids(opts) if table.is_a?(Array)
|
|
316
|
+
[ "window", annotated, options.merge(annotation: {}) ]
|
|
317
|
+
end
|
|
318
|
+
|
|
246
319
|
def extract_statement(stmt, opts)
|
|
247
320
|
if !stmt.is_a?(Array)
|
|
248
321
|
raise "Improper ConceptQL statement: Expected an Array, got a #{stmt.class}"
|
|
@@ -254,6 +327,7 @@ module ConceptQL
|
|
|
254
327
|
|
|
255
328
|
opts[:scope_opts] = (opts[:scope_opts] || {})
|
|
256
329
|
opts[:scope_opts].merge!(window_opts: stmt.last.merge(cdb: cdb))
|
|
330
|
+
@window_options = (@window_options || {}).merge(stmt.last)
|
|
257
331
|
extract_statement(stmt[1], opts)
|
|
258
332
|
elsif stmt.length == 1 && stmt.first.is_a?(Array)
|
|
259
333
|
extract_statement(stmt.first, opts)
|
data/lib/conceptql/scope.rb
CHANGED
|
@@ -90,7 +90,7 @@ module ConceptQL
|
|
|
90
90
|
# Query#annotate sets it.
|
|
91
91
|
attr_accessor :one_query_counts
|
|
92
92
|
|
|
93
|
-
attr_reader :known_operators, :recall_stack, :recall_dependencies, :
|
|
93
|
+
attr_reader :known_operators, :recall_stack, :recall_dependencies, :opts, :query_columns
|
|
94
94
|
|
|
95
95
|
def initialize(opts = {})
|
|
96
96
|
@known_operators = {}
|
|
@@ -98,11 +98,7 @@ module ConceptQL
|
|
|
98
98
|
@recall_stack = []
|
|
99
99
|
@label_cte_names = {}
|
|
100
100
|
@operator_cte_names = {}.compare_by_identity
|
|
101
|
-
@annotation = {}
|
|
102
101
|
@opts = opts.dup
|
|
103
|
-
@annotation[:errors] = @errors = {}
|
|
104
|
-
@annotation[:warnings] = @warnings = {}
|
|
105
|
-
@annotation[:counts] = @counts = {}
|
|
106
102
|
@query_columns = DEFAULT_COLUMNS.keys
|
|
107
103
|
@query_columns << :window_id if opts.dig(:window_opts, :window_table)
|
|
108
104
|
|
|
@@ -116,19 +112,6 @@ module ConceptQL
|
|
|
116
112
|
"You must set the DOCKER_SCRATCH_DATABASE environment variable to the name of the scratch database if using the CONCEPTQL_FORCE_TEMP_TABLES environment variable"
|
|
117
113
|
end
|
|
118
114
|
|
|
119
|
-
def add_errors(key, errors)
|
|
120
|
-
@errors[key] = errors
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
def add_warnings(key, errors)
|
|
124
|
-
@warnings[key] = errors
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
def add_counts(key, domain, counts)
|
|
128
|
-
c = @counts[key] ||= {}
|
|
129
|
-
c[domain] = counts
|
|
130
|
-
end
|
|
131
|
-
|
|
132
115
|
# Operator#evaluate records the name of each CTE that holds an operator's
|
|
133
116
|
# rows. An operator evaluated more than once has more than one name.
|
|
134
117
|
def record_cte(op, name)
|
data/lib/conceptql/version.rb
CHANGED
|
@@ -15,9 +15,9 @@ module ConceptQL
|
|
|
15
15
|
# whatever the event date. So a statement names the system, and the system
|
|
16
16
|
# decides by the event date. ["ms_drg", "010"] selects the DRG 010 events
|
|
17
17
|
# from 2007-10-01, and ["cms_drg", "010"] selects the DRG 010 events before
|
|
18
|
-
# that day. The concept-ID lookup is the one that drg uses
|
|
19
|
-
# operators
|
|
20
|
-
#
|
|
18
|
+
# that day. The concept-ID lookup is the one that drg uses. The
|
|
19
|
+
# operators use no OHDSI validity date and no concept class to select
|
|
20
|
+
# events.
|
|
21
21
|
#
|
|
22
22
|
# Each operator is a vocabulary operator of vocabulary DRG. So its code
|
|
23
23
|
# format is the DRG row of vocabulary_formats (a vocabulary operator uses
|
|
@@ -3,15 +3,17 @@
|
|
|
3
3
|
require "csv"
|
|
4
4
|
require_relative "entry"
|
|
5
5
|
require_relative "drg_systems"
|
|
6
|
+
require_relative "read_codes"
|
|
6
7
|
|
|
7
8
|
module ConceptQL
|
|
8
9
|
module Vocabularies
|
|
9
10
|
class DynamicVocabularies
|
|
10
|
-
#
|
|
11
|
-
# ConceptQL::Database's own (Database#operators
|
|
12
|
-
#
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
# lexicons: the Lexicons whose vocabularies become operators, normally a
|
|
12
|
+
# ConceptQL::Database's own and its reference lexicon (Database#operators
|
|
13
|
+
# passes them). A nil one is left out. Without one, only the CSV
|
|
14
|
+
# vocabularies are used.
|
|
15
|
+
def initialize(*lexicons)
|
|
16
|
+
@lexicons = lexicons.compact
|
|
15
17
|
end
|
|
16
18
|
|
|
17
19
|
# Vocabulary operators for one data model, as name => operator class.
|
|
@@ -28,7 +30,8 @@ module ConceptQL
|
|
|
28
30
|
end
|
|
29
31
|
|
|
30
32
|
# Every vocabulary entry, as id => Entry, and the entries of ms_drg and
|
|
31
|
-
# cms_drg, which copy the entry of drg (DrgSystems).
|
|
33
|
+
# cms_drg, which copy the entry of drg (DrgSystems). The entry of read
|
|
34
|
+
# gets its preferred name (ReadCodes).
|
|
32
35
|
def all_vocabs
|
|
33
36
|
@all_vocabs ||= begin
|
|
34
37
|
vocabs = each_vocab.each_with_object({}) do |row, h|
|
|
@@ -36,13 +39,14 @@ module ConceptQL
|
|
|
36
39
|
h[entry.id] ||= entry
|
|
37
40
|
h[entry.id] = h[entry.id].merge(entry)
|
|
38
41
|
end
|
|
39
|
-
vocabs.merge(DrgSystems.entries(vocabs[DrgSystems::DRG_OPERATOR])
|
|
42
|
+
vocabs.merge(DrgSystems.entries(vocabs[DrgSystems::DRG_OPERATOR]),
|
|
43
|
+
ReadCodes.entries(vocabs[ReadCodes::OPERATOR]))
|
|
40
44
|
end
|
|
41
45
|
end
|
|
42
46
|
|
|
43
47
|
private
|
|
44
48
|
|
|
45
|
-
attr_reader :
|
|
49
|
+
attr_reader :lexicons
|
|
46
50
|
|
|
47
51
|
def each_vocab
|
|
48
52
|
@each_vocab ||= get_all_vocabs
|
|
@@ -60,9 +64,10 @@ module ConceptQL
|
|
|
60
64
|
end
|
|
61
65
|
|
|
62
66
|
# A Database accepts any object as its lexicon (opts[:lexicon]); one that
|
|
63
|
-
# keeps no vocabulary list contributes no vocabulary operators.
|
|
67
|
+
# keeps no vocabulary list contributes no vocabulary operators. A
|
|
68
|
+
# vocabulary in two lexicons gets one entry (#all_vocabs merges them).
|
|
64
69
|
def lexicon_vocabularies
|
|
65
|
-
lexicon.respond_to?(:vocabularies) ? lexicon.vocabularies : []
|
|
70
|
+
lexicons.flat_map { |lexicon| lexicon.respond_to?(:vocabularies) ? lexicon.vocabularies : [] }
|
|
66
71
|
end
|
|
67
72
|
end
|
|
68
73
|
end
|
|
@@ -28,8 +28,8 @@ module ConceptQL
|
|
|
28
28
|
predominant_domains
|
|
29
29
|
]
|
|
30
30
|
|
|
31
|
-
# A blank domain is no domain (bead conceptql-7azx). The GDM
|
|
32
|
-
#
|
|
31
|
+
# A blank domain is no domain (bead conceptql-7azx). The removed GDM
|
|
32
|
+
# lexicon gave a vocabulary without a domain the domain '', and that
|
|
33
33
|
# became the criterion_domain of its operators. Without the key, the
|
|
34
34
|
# vocabulary selects condition_occurrence records (predominant_domains),
|
|
35
35
|
# and a merge keeps the other entry's domain.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../behaviors/labish"
|
|
4
|
+
|
|
5
|
+
module ConceptQL
|
|
6
|
+
module Vocabularies
|
|
7
|
+
# The read operator: the vocabulary operator of the Read vocabulary (NHS UK
|
|
8
|
+
# Read Codes Version 2), built from its row of config/vocabularies.csv like
|
|
9
|
+
# every other vocabulary operator (bead conceptql-qdn9). Before, read was
|
|
10
|
+
# an operator class that wrapped one Vocabulary operator, so it got no
|
|
11
|
+
# vocabulary check. This module keeps the three things that the row
|
|
12
|
+
# cannot say: the preferred name READ, read's own description, and the
|
|
13
|
+
# lab columns (Labish). A Read code can carry a test result, so read
|
|
14
|
+
# selects the value columns although its domain is condition_occurrence.
|
|
15
|
+
#
|
|
16
|
+
# Read codes are case-sensitive: "13g4.00" (Born in Burkina Faso) and
|
|
17
|
+
# "13G4.00" (Social worker involved) are two codes. Every vocabulary
|
|
18
|
+
# operator compares codes case-exactly, so a code in another case selects
|
|
19
|
+
# nothing and is an unknown code.
|
|
20
|
+
module ReadCodes
|
|
21
|
+
OPERATOR = "read"
|
|
22
|
+
PREFERRED_NAME = "READ"
|
|
23
|
+
DESC = "Selects records based on the Read vocabulary."
|
|
24
|
+
|
|
25
|
+
# The entry of read: the entry of the Read vocabulary with the preferred
|
|
26
|
+
# name READ, as id => Entry. Without a Read entry there is none.
|
|
27
|
+
def self.entries(read_entry)
|
|
28
|
+
return {} unless read_entry
|
|
29
|
+
|
|
30
|
+
{ OPERATOR => read_entry.merge(Entry.new(id: read_entry.omopv5_id, preferred_name: PREFERRED_NAME)) }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# The behavior of the read operator class
|
|
34
|
+
module OperatorBehavior
|
|
35
|
+
def self.included(base)
|
|
36
|
+
base.desc DESC
|
|
37
|
+
base.include ConceptQL::Behaviors::Labish
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -4,6 +4,7 @@ require_relative "../behaviors/drugish"
|
|
|
4
4
|
require_relative "../behaviors/labish"
|
|
5
5
|
require_relative "behaviors/gdmish"
|
|
6
6
|
require_relative "drg_systems"
|
|
7
|
+
require_relative "read_codes"
|
|
7
8
|
require_relative "../operators/vocabulary"
|
|
8
9
|
|
|
9
10
|
module ConceptQL
|
|
@@ -66,6 +67,8 @@ module ConceptQL
|
|
|
66
67
|
|
|
67
68
|
include DrgSystems::OperatorBehavior if DrgSystems.system(entry.id)
|
|
68
69
|
|
|
70
|
+
include ReadCodes::OperatorBehavior if entry.id == ReadCodes::OPERATOR
|
|
71
|
+
|
|
69
72
|
deprecated DrgSystems::DRG_DEPRECATION if entry.id == DrgSystems::DRG_OPERATOR
|
|
70
73
|
|
|
71
74
|
def vocab_entry
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: conceptql
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ryan Duryea
|
|
@@ -390,6 +390,7 @@ files:
|
|
|
390
390
|
- docs/spec/examples/union-operator.json
|
|
391
391
|
- docs/spec/examples/vocabulary-operator.json
|
|
392
392
|
- lib/conceptql.rb
|
|
393
|
+
- lib/conceptql/annotation.rb
|
|
393
394
|
- lib/conceptql/behaviors/code_lister.rb
|
|
394
395
|
- lib/conceptql/behaviors/drugish.rb
|
|
395
396
|
- lib/conceptql/behaviors/labish.rb
|
|
@@ -423,7 +424,6 @@ files:
|
|
|
423
424
|
- lib/conceptql/diagram_renderer/svg.rb
|
|
424
425
|
- lib/conceptql/diagram_renderer/tree_layout.rb
|
|
425
426
|
- lib/conceptql/lexicon.rb
|
|
426
|
-
- lib/conceptql/lexicon/lexicon_gdm.rb
|
|
427
427
|
- lib/conceptql/lexicon/lexicon_no_db.rb
|
|
428
428
|
- lib/conceptql/lexicon/lexicon_ohdsi.rb
|
|
429
429
|
- lib/conceptql/lexicon/lexicon_strategy.rb
|
|
@@ -468,7 +468,6 @@ files:
|
|
|
468
468
|
- lib/conceptql/operators/provenance.rb
|
|
469
469
|
- lib/conceptql/operators/provider_filter.rb
|
|
470
470
|
- lib/conceptql/operators/race.rb
|
|
471
|
-
- lib/conceptql/operators/read.rb
|
|
472
471
|
- lib/conceptql/operators/recall.rb
|
|
473
472
|
- lib/conceptql/operators/snf.rb
|
|
474
473
|
- lib/conceptql/operators/sum.rb
|
|
@@ -508,6 +507,7 @@ files:
|
|
|
508
507
|
- lib/conceptql/vocabularies/dynamic_vocabularies.rb
|
|
509
508
|
- lib/conceptql/vocabularies/entry.rb
|
|
510
509
|
- lib/conceptql/vocabularies/formats.rb
|
|
510
|
+
- lib/conceptql/vocabularies/read_codes.rb
|
|
511
511
|
- lib/conceptql/vocabularies/vocabulary_class_factory.rb
|
|
512
512
|
- lib/conceptql/window.rb
|
|
513
513
|
- lib/conceptql/window/base.rb
|