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,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lexicon_strategy"
|
|
4
|
+
|
|
5
|
+
module ConceptQL
|
|
6
|
+
class LexiconOhdsi < LexiconStrategy
|
|
7
|
+
class << self
|
|
8
|
+
def db_has_all_vocabulary_tables?(db)
|
|
9
|
+
%i[concept_ancestor concept concept_relationship vocabulary].all? { |t| db.table_exists?(t) }
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def vocabularies_query
|
|
14
|
+
db[:vocabulary]
|
|
15
|
+
.select(Sequel[:vocabulary_id].as(:id),
|
|
16
|
+
Sequel[:vocabulary_id].as(:omopv5_vocabulary_id),
|
|
17
|
+
Sequel[:vocabulary_name].as(:vocabulary_short_name),
|
|
18
|
+
Sequel[:vocabulary_name].as(:vocabulary_full_name),
|
|
19
|
+
Sequel[nil].as(:domain),
|
|
20
|
+
Sequel.expr(1).as(:from_lexicon))
|
|
21
|
+
.from_self
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def vocabularies
|
|
25
|
+
vocabularies_query.all
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def is_a_relationships(_data_db)
|
|
29
|
+
db[:concept_relationship]
|
|
30
|
+
.select(
|
|
31
|
+
Sequel[:concept_id_1].as(:concept_1_id),
|
|
32
|
+
Sequel[:concept_id_2].as(:concept_2_id),
|
|
33
|
+
:relationship_id
|
|
34
|
+
)
|
|
35
|
+
.where { Sequel.function(:lower, :relationship_id) =~ "is a" }
|
|
36
|
+
.from_self
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def ancestors_table(_data_db)
|
|
40
|
+
db[:concept_ancestor].select(
|
|
41
|
+
Sequel[:ancestor_concept_id].as(:ancestor_id),
|
|
42
|
+
Sequel[:descendant_concept_id].as(:descendant_id)
|
|
43
|
+
).from_self
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def concepts_table(_data_db, _some_schema = nil)
|
|
47
|
+
db[:concept].select(
|
|
48
|
+
Sequel[:concept_id].as(:id),
|
|
49
|
+
:concept_code,
|
|
50
|
+
:vocabulary_id,
|
|
51
|
+
Sequel[:concept_name].as(:concept_text)
|
|
52
|
+
).from_self
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def table_is_missing?(_data_db)
|
|
56
|
+
!db.table_exists?(:concept)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def strategy
|
|
60
|
+
:ohdsi
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ConceptQL
|
|
4
|
+
class LexiconStrategy
|
|
5
|
+
attr_reader :db
|
|
6
|
+
|
|
7
|
+
def initialize(db)
|
|
8
|
+
@db = db
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# A lookup is always scoped to a vocabulary. `vocabulary_id` is polymorphic:
|
|
12
|
+
# a String, an Integer (ReadOmop reports 17), or an Array of vocabulary names
|
|
13
|
+
# (the provenance-type pairs), which Sequel turns into an IN clause.
|
|
14
|
+
#
|
|
15
|
+
# There is deliberately no wildcard. `'*'` is meaningful elsewhere in the
|
|
16
|
+
# operator family, but only in `arguments` — Vocabulary#select_all? reads it
|
|
17
|
+
# there and *skips* the lexicon call entirely. An earlier version of this
|
|
18
|
+
# method honoured `'*'` here too, which no caller ever used and which read as
|
|
19
|
+
# support for cross-vocabulary lookup that does not exist.
|
|
20
|
+
def concepts(data_db, vocabulary_id, codes = [])
|
|
21
|
+
if vocabulary_id.blank? || Array(vocabulary_id).include?("*")
|
|
22
|
+
raise ArgumentError,
|
|
23
|
+
"concepts requires a specific vocabulary, got #{vocabulary_id.inspect}. " \
|
|
24
|
+
"There is no wildcard lookup; pass a vocabulary name, an Array of them, " \
|
|
25
|
+
"or an id. ('*' belongs in arguments, where Vocabulary#select_all? handles it.)"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
ds = concepts_table(data_db)
|
|
29
|
+
|
|
30
|
+
ds = ds.where(vocabulary_id: vocabulary_id)
|
|
31
|
+
ds = ds.where(Sequel.function(:lower, :concept_code) => Array(codes).map(&:downcase)) unless codes.blank?
|
|
32
|
+
|
|
33
|
+
ds
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def concepts_by_name(data_db, names = [])
|
|
37
|
+
ds = concepts_table(data_db)
|
|
38
|
+
|
|
39
|
+
ds.where(Sequel.function(:lower, :concept_text) => Array(names).map(&:downcase))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def descendants_of(_data_db, concept_ids_or_ds)
|
|
43
|
+
where_values = Array(concept_ids_or_ds).flatten.dup
|
|
44
|
+
|
|
45
|
+
descendants = ancestors_table(db)
|
|
46
|
+
.where(ancestor_id: where_values)
|
|
47
|
+
.select(:descendant_id)
|
|
48
|
+
|
|
49
|
+
unless where_values.empty?
|
|
50
|
+
union_clause = db.values(where_values.map { |v| [ v ] })
|
|
51
|
+
descendants = descendants.union(union_clause).distinct
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
descendants.select_map(:descendant_id)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def concept_ids(_data_db, vocabulary_id, codes = [])
|
|
58
|
+
concepts(db, vocabulary_id, codes)
|
|
59
|
+
.select_map(:id)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Every concept code in the vocabulary (or vocabularies).
|
|
63
|
+
def concept_codes(data_db, vocabulary_id)
|
|
64
|
+
concepts(data_db, vocabulary_id).select_map(:concept_code)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def concept_ids_by_name(data_db, names)
|
|
68
|
+
concepts_by_name(data_db, names).select_map(:id)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# The mappings table will tell us what other concepts have been directly
|
|
72
|
+
# mapped to the concepts passed in
|
|
73
|
+
def related_concept_ids(data_db, *ids)
|
|
74
|
+
ids = ids.flatten
|
|
75
|
+
other_ids = is_a_relationships(data_db)
|
|
76
|
+
.where(concept_2_id: ids)
|
|
77
|
+
.select_map(:concept_1_id)
|
|
78
|
+
other_ids + ids
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# A caller can answer "is the vocabulary empty?" with a block. The
|
|
82
|
+
# Lexicon does, so that the answer comes from its cache.
|
|
83
|
+
def known_codes(data_db, vocabulary_id, codes)
|
|
84
|
+
return codes if db_is_mock?(data_db)
|
|
85
|
+
return codes if block_given? ? yield : vocabulary_is_empty?(data_db, vocabulary_id)
|
|
86
|
+
|
|
87
|
+
concepts_ds(data_db, vocabulary_id, codes).select_map(:concept_code)
|
|
88
|
+
rescue Sequel::DatabaseError
|
|
89
|
+
[]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# A caller can answer "is the concept table missing?" with a block. The
|
|
93
|
+
# Lexicon does, so that the answer comes from its cache.
|
|
94
|
+
def concepts_to_codes(data_db, vocabulary_id, codes = [])
|
|
95
|
+
missing = db.nil? || (block_given? ? yield : table_is_missing?(data_db))
|
|
96
|
+
return codes.map { |code| [ code, nil ] } if missing
|
|
97
|
+
|
|
98
|
+
concepts(db, vocabulary_id, codes).select_map(%i[concept_code concept_text])
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def vocabulary_is_empty?(data_db, vocabulary_id)
|
|
102
|
+
concepts_table(data_db).where(vocabulary_id: vocabulary_id).count.zero?
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def concepts_ds(data_db, vocabulary_id, codes)
|
|
106
|
+
concepts_table(data_db)
|
|
107
|
+
.where(vocabulary_id: vocabulary_id, concept_code: codes)
|
|
108
|
+
.select(Sequel[:concept_code].as(:concept_code), Sequel[:concept_text].as(:concept_text))
|
|
109
|
+
.from_self
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def db_is_mock?(_data_db)
|
|
113
|
+
db.is_a?(Sequel::Mock::Database)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "forwardable"
|
|
4
|
+
require_relative "lexicon/lexicon_gdm"
|
|
5
|
+
require_relative "lexicon/lexicon_no_db"
|
|
6
|
+
require_relative "lexicon/lexicon_ohdsi"
|
|
7
|
+
|
|
8
|
+
module ConceptQL
|
|
9
|
+
class Lexicon
|
|
10
|
+
extend Forwardable
|
|
11
|
+
|
|
12
|
+
# Lookups that return plain values. With cache: true each result is kept
|
|
13
|
+
# for the life of this Lexicon, which is the life of its Database, so the
|
|
14
|
+
# cache never outlives one export (seed conceptql-a5f). Methods that return
|
|
15
|
+
# datasets are not cached: their query runs later, in the caller.
|
|
16
|
+
CACHED_LOOKUPS = %i[
|
|
17
|
+
concept_codes
|
|
18
|
+
concept_ids
|
|
19
|
+
concept_ids_by_name
|
|
20
|
+
concepts_to_codes
|
|
21
|
+
descendants_of
|
|
22
|
+
known_codes
|
|
23
|
+
related_concept_ids
|
|
24
|
+
table_is_missing?
|
|
25
|
+
vocabulary_is_empty?
|
|
26
|
+
].freeze
|
|
27
|
+
|
|
28
|
+
def_delegators :@strategy,
|
|
29
|
+
:strategy,
|
|
30
|
+
:concepts,
|
|
31
|
+
:concepts_table,
|
|
32
|
+
:concepts_by_name,
|
|
33
|
+
:concepts_ds,
|
|
34
|
+
:vocabularies,
|
|
35
|
+
:vocabularies_query
|
|
36
|
+
|
|
37
|
+
# The cached results keyed on [method, arguments], or nil when caching is
|
|
38
|
+
# off. Its size is the number of distinct lookups.
|
|
39
|
+
attr_reader :cache
|
|
40
|
+
|
|
41
|
+
def initialize(lexicon_db, dataset_db = nil, strategy: nil, cache: false)
|
|
42
|
+
@strategy = strategy || determine_strategy(lexicon_db, dataset_db)
|
|
43
|
+
@cache = {} if cache
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
(CACHED_LOOKUPS - %i[concepts_to_codes known_codes table_is_missing?]).each do |name|
|
|
47
|
+
define_method(name) do |*args|
|
|
48
|
+
lookup(name, args) { @strategy.public_send(name, *args) }
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The strategy's known_codes first asks whether the vocabulary is empty.
|
|
53
|
+
# It asks through the block, so that question also reaches the cache.
|
|
54
|
+
def known_codes(data_db, vocabulary_id, codes)
|
|
55
|
+
lookup(:known_codes, [ data_db, vocabulary_id, codes ]) do
|
|
56
|
+
@strategy.known_codes(data_db, vocabulary_id, codes) { vocabulary_is_empty?(data_db, vocabulary_id) }
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# The strategy's concepts_to_codes first asks whether the concept table is
|
|
61
|
+
# missing. It asks through the block, so that question also reaches the cache.
|
|
62
|
+
def concepts_to_codes(data_db, vocabulary_id, codes = [])
|
|
63
|
+
lookup(:concepts_to_codes, [ data_db, vocabulary_id, codes ]) do
|
|
64
|
+
@strategy.concepts_to_codes(data_db, vocabulary_id, codes) { table_is_missing?(data_db) }
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Only "the table is there" is kept. A missing table (or a database error,
|
|
69
|
+
# which Sequel's table_exists? turns into "missing") is checked again on
|
|
70
|
+
# the next call, so one transient error does not last the whole export.
|
|
71
|
+
def table_is_missing?(data_db)
|
|
72
|
+
lookup(:table_is_missing?, [ data_db ], keep: ->(missing) { !missing }) do
|
|
73
|
+
@strategy.table_is_missing?(data_db)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Runs the lookup, or with caching on returns the result kept for the same
|
|
78
|
+
# method and arguments. A result that keep rejects is returned, not kept.
|
|
79
|
+
def lookup(name, args, keep: nil)
|
|
80
|
+
return yield unless cache
|
|
81
|
+
|
|
82
|
+
# Copy Array arguments so a caller that mutates one later cannot
|
|
83
|
+
# change a key. Results are frozen so a mutation fails loudly.
|
|
84
|
+
key = [ name, args.map { |arg| arg.is_a?(Array) ? arg.dup.freeze : arg } ]
|
|
85
|
+
cache.fetch(key) do
|
|
86
|
+
result = yield.dup.freeze
|
|
87
|
+
cache[key] = result if keep.nil? || keep.call(result)
|
|
88
|
+
result
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
private :lookup
|
|
92
|
+
|
|
93
|
+
def determine_strategy(lexicon_db, dataset_db)
|
|
94
|
+
[ dataset_db, lexicon_db ].compact.each do |db|
|
|
95
|
+
lexicon_classes.each do |klass|
|
|
96
|
+
next unless klass.db_has_all_vocabulary_tables?(db)
|
|
97
|
+
|
|
98
|
+
return (klass.new(db))
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
LexiconNoDB.new(Sequel.mock(host: :postgres))
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def lexicon_classes
|
|
105
|
+
classes = []
|
|
106
|
+
classes << LexiconOhdsi unless ENV["LEXICON_GDM_ONLY"]
|
|
107
|
+
classes << LexiconGDM unless ENV["LEXICON_OHDSI_ONLY"]
|
|
108
|
+
classes
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
data/lib/conceptql/nodifier.rb
CHANGED
|
@@ -1,36 +1,77 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "operators/operator"
|
|
4
4
|
|
|
5
5
|
module ConceptQL
|
|
6
6
|
class Nodifier
|
|
7
|
-
attr_reader :
|
|
8
|
-
|
|
9
|
-
def initialize
|
|
10
|
-
@
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
7
|
+
attr_reader :cdb, :scope, :data_model, :database_type, :algorithm_fetcher
|
|
8
|
+
|
|
9
|
+
def initialize(cdb, opts = {})
|
|
10
|
+
@cdb = cdb
|
|
11
|
+
@scope = opts[:scope] || Scope.new(opts.delete(:scope_opts) || {})
|
|
12
|
+
@data_model = get_data_model(opts)
|
|
13
|
+
@database_type = opts[:database_type] || cdb.database_type
|
|
14
|
+
@algorithm_fetcher = opts[:algorithm_fetcher] || (proc do |_alg|
|
|
15
|
+
nil
|
|
16
|
+
end)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def get_data_model(opts)
|
|
20
|
+
(opts[:data_model] || cdb.opts[:data_model] || ENV["CONCEPTQL_DATA_MODEL"] || ConceptQL::DEFAULT_DATA_MODEL).to_sym
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def create(operator, *values)
|
|
24
|
+
operator = operator.to_s.downcase
|
|
25
|
+
if operator == "algorithm"
|
|
26
|
+
statement, = algorithm_fetcher.call(values.first)
|
|
27
|
+
if statement
|
|
28
|
+
create(*statement)
|
|
29
|
+
else
|
|
30
|
+
invalid_op(operator, values, "invalid algorithm", values.first)
|
|
18
31
|
end
|
|
32
|
+
elsif (klass = fetch_op(operator))
|
|
33
|
+
klass.new(self, operator, *values)
|
|
34
|
+
else
|
|
35
|
+
invalid_op(operator, values, "invalid operator", operator)
|
|
19
36
|
end
|
|
20
37
|
end
|
|
21
38
|
|
|
22
|
-
def
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
39
|
+
def to_metadata(opts = {})
|
|
40
|
+
Hash[operators.map do |k, v|
|
|
41
|
+
[ k, v.to_metadata(k, opts) ]
|
|
42
|
+
end.reject { |_k, v| v[:categories].empty? }.sort_by { |_k, v| v[:name] }]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# The operators this nodifier can create: its Database's registry for
|
|
46
|
+
# its data model.
|
|
47
|
+
def operators
|
|
48
|
+
@operators ||= cdb.operators(@data_model)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def fetch_op(operator)
|
|
54
|
+
operators[alias_for(operator)]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def alias_for(operator)
|
|
58
|
+
operator_aliases[operator] || operator
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def operator_aliases
|
|
62
|
+
@operator_aliases ||= operators.flat_map do |id, klass|
|
|
63
|
+
next unless klass.aliases.present?
|
|
64
|
+
|
|
65
|
+
klass.aliases.map { |klass_alias| [ klass_alias, id ] }
|
|
66
|
+
end.compact.to_h
|
|
30
67
|
end
|
|
31
68
|
|
|
32
|
-
def
|
|
33
|
-
|
|
69
|
+
def invalid_op(operator, values, *error_args)
|
|
70
|
+
options = values.pop if values.last.is_a?(Hash)
|
|
71
|
+
options ||= {}
|
|
72
|
+
options = options.merge(errors: [ error_args ])
|
|
73
|
+
values << options
|
|
74
|
+
Operators::Invalid.new(self, operator, *values)
|
|
34
75
|
end
|
|
35
76
|
end
|
|
36
77
|
end
|
|
@@ -1,25 +1,45 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "temporal_operator"
|
|
2
4
|
|
|
3
5
|
module ConceptQL
|
|
4
6
|
module Operators
|
|
5
7
|
class After < TemporalOperator
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
For
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
register __FILE__
|
|
9
|
+
|
|
10
|
+
desc "For each person, passes along left hand records with a start_date that occurs after the earliest end_date of a right hand record."
|
|
11
|
+
|
|
12
|
+
allows_at_least_option
|
|
13
|
+
within_skip :after
|
|
14
|
+
|
|
15
|
+
def right_stream_query(db)
|
|
16
|
+
if compare_all?
|
|
17
|
+
right.evaluate(db).from_self
|
|
18
|
+
else
|
|
19
|
+
right.evaluate(db).from_self.select_group(*matching_columns).select_append(Sequel.function(:min,
|
|
20
|
+
:end_date).as(:end_date))
|
|
21
|
+
end
|
|
17
22
|
end
|
|
18
23
|
|
|
19
24
|
def where_clause
|
|
20
|
-
|
|
25
|
+
after_date = r_end_date
|
|
26
|
+
|
|
27
|
+
after_date = adjust_date(at_least_option, after_date) if at_least_option
|
|
28
|
+
|
|
29
|
+
clause = Sequel.expr(l_start_date > after_date)
|
|
30
|
+
|
|
31
|
+
clause &= l_start_date <= within_end if within_option
|
|
32
|
+
|
|
33
|
+
clause
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def compare_all?
|
|
37
|
+
!(options.keys & [ :within ]).empty?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def rhs_function
|
|
41
|
+
compare_all? ? nil : :min
|
|
21
42
|
end
|
|
22
43
|
end
|
|
23
44
|
end
|
|
24
45
|
end
|
|
25
|
-
|
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "temporal_operator"
|
|
2
4
|
|
|
3
5
|
module ConceptQL
|
|
4
6
|
module Operators
|
|
5
7
|
class AnyOverlap < TemporalOperator
|
|
6
|
-
|
|
8
|
+
register __FILE__
|
|
9
|
+
|
|
10
|
+
desc "For each person, passes along left hand records with a date range that overlaps in any way with a right hand record's date_range."
|
|
11
|
+
|
|
7
12
|
def where_clause
|
|
8
|
-
|
|
9
|
-
r_partly_in_l = Sequel.expr { l__start_date <= r__start_date }.&(Sequel.expr { r__start_date <= l__end_date })
|
|
10
|
-
l_partly_in_r.|(r_partly_in_l)
|
|
13
|
+
((within_start <= l_start_date) & (l_start_date <= within_end)) | ((l_start_date <= within_start) & (within_start <= l_end_date))
|
|
11
14
|
end
|
|
12
15
|
end
|
|
13
16
|
end
|
|
14
17
|
end
|
|
15
|
-
|
|
@@ -1,20 +1,44 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "temporal_operator"
|
|
2
4
|
|
|
3
5
|
module ConceptQL
|
|
4
6
|
module Operators
|
|
5
7
|
class Before < TemporalOperator
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
For
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
register __FILE__
|
|
9
|
+
|
|
10
|
+
desc "For each person, passes along left hand records with an end_date that occurs before the most recent start_date of a right hand record."
|
|
11
|
+
|
|
12
|
+
allows_at_least_option
|
|
13
|
+
within_skip :before
|
|
14
|
+
|
|
15
|
+
def right_stream_query(db)
|
|
16
|
+
if compare_all?
|
|
17
|
+
right.evaluate(db).from_self
|
|
18
|
+
else
|
|
19
|
+
right.evaluate(db).from_self.select_group(*matching_columns).select_append(Sequel.function(:max,
|
|
20
|
+
:start_date).as(:start_date))
|
|
21
|
+
end
|
|
14
22
|
end
|
|
15
23
|
|
|
16
24
|
def where_clause
|
|
17
|
-
|
|
25
|
+
before_date = r_start_date
|
|
26
|
+
|
|
27
|
+
before_date = adjust_date(at_least_option, before_date, true) if at_least_option
|
|
28
|
+
|
|
29
|
+
before_clause = Sequel.expr(l_end_date < before_date)
|
|
30
|
+
|
|
31
|
+
before_clause &= l_end_date >= within_start if within_option
|
|
32
|
+
|
|
33
|
+
before_clause
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def compare_all?
|
|
37
|
+
!(options.keys & [ :within ]).empty?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def rhs_function
|
|
41
|
+
compare_all? ? nil : :min
|
|
18
42
|
end
|
|
19
43
|
end
|
|
20
44
|
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "operator"
|
|
3
4
|
|
|
4
5
|
module ConceptQL
|
|
5
6
|
module Operators
|
|
@@ -7,39 +8,81 @@ module ConceptQL
|
|
|
7
8
|
class BinaryOperatorOperator < Operator
|
|
8
9
|
option :left, type: :upstream
|
|
9
10
|
option :right, type: :upstream
|
|
11
|
+
validate_no_arguments
|
|
12
|
+
validate_option Array, :left, :right
|
|
13
|
+
validate_required_options :left, :right
|
|
14
|
+
category "Filter by Comparing"
|
|
15
|
+
basic_type :filter
|
|
10
16
|
|
|
11
17
|
def upstreams
|
|
12
|
-
[left]
|
|
18
|
+
[ left ]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def code_list(db)
|
|
22
|
+
left.code_list(db) + right.code_list(db)
|
|
13
23
|
end
|
|
14
24
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
me = g.send(cluster_name) do |sub|
|
|
20
|
-
sub[rank: 'same', label: display_name, color: 'black']
|
|
21
|
-
sub.send("#{cluster_name}_left").send('[]', shape: 'point', color: type_color(types))
|
|
22
|
-
sub.send("#{cluster_name}_right").send('[]', shape: 'point')
|
|
23
|
-
end
|
|
24
|
-
left.link_to(g, me.send("#{cluster_name}_left"), db)
|
|
25
|
-
right.link_to(g, me.send("#{cluster_name}_right"), db)
|
|
26
|
-
@__graph_operator = me.send("#{cluster_name}_left")
|
|
25
|
+
attr_reader :left, :right
|
|
26
|
+
|
|
27
|
+
def events_per_patient
|
|
28
|
+
left.events_per_patient
|
|
27
29
|
end
|
|
28
30
|
|
|
29
|
-
def
|
|
30
|
-
|
|
31
|
+
def multiple_vocabularies
|
|
32
|
+
left.multiple_vocabularies
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def accept(visitor, opts = {})
|
|
36
|
+
visitor.visit(self)
|
|
37
|
+
left.accept(visitor, opts)
|
|
38
|
+
right.accept(visitor, opts) unless opts[:avoid_rhs]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def all_upstreams
|
|
42
|
+
[ left, right ]
|
|
31
43
|
end
|
|
32
44
|
|
|
33
45
|
private
|
|
34
|
-
|
|
35
|
-
|
|
46
|
+
|
|
47
|
+
def pp_upstreams
|
|
48
|
+
{ left: left, right: right }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def join_columns(opts = {})
|
|
52
|
+
join_columns_option.map { |c| Sequel.expr([ [ Sequel[:l][c], Sequel[opts[:qualifier] || :r][c] ] ]) }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def join_columns_option
|
|
56
|
+
(column_names_option(:join_columns) || []) + matching_columns
|
|
36
57
|
end
|
|
37
58
|
|
|
38
|
-
def
|
|
39
|
-
|
|
59
|
+
def annotate_values(db, opts = {})
|
|
60
|
+
h = {}
|
|
61
|
+
h[:left] = left.annotate(db, opts) if left
|
|
62
|
+
h[:right] = right.annotate(db, opts) if right
|
|
63
|
+
[ options.merge(h), *arguments ]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def create_upstreams
|
|
67
|
+
@left = to_op(options[:left]) if options[:left].is_a?(Array)
|
|
68
|
+
@right = to_op(options[:right]) if options[:right].is_a?(Array)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def left_stream(db)
|
|
72
|
+
left_stream_query(db)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def left_stream_query(db)
|
|
76
|
+
left.evaluate(db).from_self(alias: :l)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def right_stream(db)
|
|
80
|
+
right_stream_query(db).as(:r)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def right_stream_query(db)
|
|
84
|
+
right.evaluate(db).from_self(alias: :r)
|
|
40
85
|
end
|
|
41
86
|
end
|
|
42
87
|
end
|
|
43
88
|
end
|
|
44
|
-
|
|
45
|
-
|