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.
Files changed (242) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +226 -117
  3. data/LICENSE +21 -0
  4. data/README.md +35 -83
  5. data/bin/conceptql +2 -1
  6. data/bin/conceptql_utils +6 -0
  7. data/config/multiple_vocabularies.csv +4 -0
  8. data/config/provenance.yml +489 -0
  9. data/config/race.yml +50 -0
  10. data/config/vocabularies.csv +87 -0
  11. data/lib/conceptql/behaviors/code_lister.rb +23 -0
  12. data/lib/conceptql/behaviors/drugish.rb +17 -0
  13. data/lib/conceptql/behaviors/labish.rb +19 -0
  14. data/lib/conceptql/behaviors/metadatable.rb +226 -73
  15. data/lib/conceptql/behaviors/provenanceable.rb +26 -0
  16. data/lib/conceptql/behaviors/single_event.rb +11 -0
  17. data/lib/conceptql/behaviors/timeless.rb +11 -0
  18. data/lib/conceptql/behaviors/utilizable.rb +206 -0
  19. data/lib/conceptql/behaviors/windowable.rb +27 -0
  20. data/lib/conceptql/cli.rb +117 -108
  21. data/lib/conceptql/columnizer.rb +75 -0
  22. data/lib/conceptql/data_model/base.rb +462 -0
  23. data/lib/conceptql/data_model/gdm.rb +155 -0
  24. data/lib/conceptql/data_model/gdm_wide.rb +42 -0
  25. data/lib/conceptql/data_model/omopv4_plus.rb +12 -0
  26. data/lib/conceptql/data_model.rb +30 -0
  27. data/lib/conceptql/database.rb +214 -0
  28. data/lib/conceptql/date_adjuster.rb +66 -18
  29. data/lib/conceptql/diagram.rb +243 -0
  30. data/lib/conceptql/lexicon/lexicon_gdm.rb +47 -0
  31. data/lib/conceptql/lexicon/lexicon_no_db.rb +49 -0
  32. data/lib/conceptql/lexicon/lexicon_ohdsi.rb +63 -0
  33. data/lib/conceptql/lexicon/lexicon_strategy.rb +116 -0
  34. data/lib/conceptql/lexicon.rb +111 -0
  35. data/lib/conceptql/nodifier.rb +65 -24
  36. data/lib/conceptql/operators/after.rb +34 -14
  37. data/lib/conceptql/operators/any_overlap.rb +8 -6
  38. data/lib/conceptql/operators/before.rb +34 -10
  39. data/lib/conceptql/operators/binary_operator_operator.rb +66 -23
  40. data/lib/conceptql/operators/casting_operator.rb +51 -36
  41. data/lib/conceptql/operators/co_reported.rb +111 -0
  42. data/lib/conceptql/operators/concurrent_within.rb +69 -0
  43. data/lib/conceptql/operators/contains.rb +12 -8
  44. data/lib/conceptql/operators/count.rb +17 -9
  45. data/lib/conceptql/operators/date_range.rb +48 -14
  46. data/lib/conceptql/operators/death.rb +14 -4
  47. data/lib/conceptql/operators/dedup.rb +31 -0
  48. data/lib/conceptql/operators/during.rb +7 -12
  49. data/lib/conceptql/operators/episode.rb +146 -0
  50. data/lib/conceptql/operators/equal.rb +9 -3
  51. data/lib/conceptql/operators/ethnicity.rb +49 -0
  52. data/lib/conceptql/operators/except.rb +19 -12
  53. data/lib/conceptql/operators/filter.rb +23 -8
  54. data/lib/conceptql/operators/first.rb +16 -3
  55. data/lib/conceptql/operators/from.rb +52 -4
  56. data/lib/conceptql/operators/gender.rb +34 -11
  57. data/lib/conceptql/operators/hospice.rb +19 -0
  58. data/lib/conceptql/operators/hospitalization.rb +19 -0
  59. data/lib/conceptql/operators/information_periods.rb +36 -0
  60. data/lib/conceptql/operators/invalid.rb +35 -0
  61. data/lib/conceptql/operators/last.rb +16 -3
  62. data/lib/conceptql/operators/match.rb +91 -0
  63. data/lib/conceptql/operators/multiple_vocabularies.rb +172 -0
  64. data/lib/conceptql/operators/numeric.rb +65 -23
  65. data/lib/conceptql/operators/numeric_filter.rb +68 -0
  66. data/lib/conceptql/operators/occurrence.rb +153 -32
  67. data/lib/conceptql/operators/one_in_two_out.rb +237 -38
  68. data/lib/conceptql/operators/operator.rb +624 -178
  69. data/lib/conceptql/operators/pass_thru.rb +17 -3
  70. data/lib/conceptql/operators/person.rb +18 -6
  71. data/lib/conceptql/operators/person_filter.rb +9 -3
  72. data/lib/conceptql/operators/place_of_service_filter.rb +43 -0
  73. data/lib/conceptql/operators/provenance.rb +55 -0
  74. data/lib/conceptql/operators/provider_filter.rb +62 -0
  75. data/lib/conceptql/operators/race.rb +55 -7
  76. data/lib/conceptql/operators/read.rb +168 -0
  77. data/lib/conceptql/operators/recall.rb +49 -14
  78. data/lib/conceptql/operators/snf.rb +19 -0
  79. data/lib/conceptql/operators/sum.rb +29 -12
  80. data/lib/conceptql/operators/temporal_operator.rb +108 -16
  81. data/lib/conceptql/operators/time_window.rb +20 -32
  82. data/lib/conceptql/operators/trim_date.rb +71 -0
  83. data/lib/conceptql/operators/trim_date_end.rb +28 -40
  84. data/lib/conceptql/operators/trim_date_start.rb +28 -41
  85. data/lib/conceptql/operators/union.rb +67 -6
  86. data/lib/conceptql/operators/vocabulary.rb +151 -0
  87. data/lib/conceptql/paths.rb +41 -0
  88. data/lib/conceptql/query.rb +191 -26
  89. data/lib/conceptql/query_modifiers/gdm/admission_date_query_modifier.rb +46 -0
  90. data/lib/conceptql/query_modifiers/gdm/drug_query_modifier.rb +54 -0
  91. data/lib/conceptql/query_modifiers/gdm/lab_query_modifier.rb +57 -0
  92. data/lib/conceptql/query_modifiers/gdm/pos_query_modifier.rb +46 -0
  93. data/lib/conceptql/query_modifiers/gdm/provenance_query_modifier.rb +45 -0
  94. data/lib/conceptql/query_modifiers/gdm/provider_query_modifier.rb +53 -0
  95. data/lib/conceptql/query_modifiers/gdm_wide/admission_date_query_modifier.rb +42 -0
  96. data/lib/conceptql/query_modifiers/gdm_wide/drug_query_modifier.rb +52 -0
  97. data/lib/conceptql/query_modifiers/gdm_wide/lab_query_modifier.rb +55 -0
  98. data/lib/conceptql/query_modifiers/gdm_wide/pos_query_modifier.rb +37 -0
  99. data/lib/conceptql/query_modifiers/gdm_wide/provenance_query_modifier.rb +38 -0
  100. data/lib/conceptql/query_modifiers/omopv4_plus/admission_date_query_modifier.rb +45 -0
  101. data/lib/conceptql/query_modifiers/omopv4_plus/drug_query_modifier.rb +80 -0
  102. data/lib/conceptql/query_modifiers/omopv4_plus/pos_query_modifier.rb +47 -0
  103. data/lib/conceptql/query_modifiers/omopv4_plus/provenance_query_modifier.rb +45 -0
  104. data/lib/conceptql/query_modifiers/omopv4_plus/provider_query_modifier.rb +26 -0
  105. data/lib/conceptql/query_modifiers/query_modifier.rb +20 -0
  106. data/lib/conceptql/rdbms/duckdb.rb +25 -0
  107. data/lib/conceptql/rdbms/generic.rb +120 -0
  108. data/lib/conceptql/rdbms/postgres.rb +51 -0
  109. data/lib/conceptql/rdbms/spark.rb +22 -0
  110. data/lib/conceptql/rdbms.rb +22 -0
  111. data/lib/conceptql/scope.rb +481 -31
  112. data/lib/conceptql/sql_formatters/formatter.rb +25 -0
  113. data/lib/conceptql/sql_formatters.rb +22 -0
  114. data/lib/conceptql/utils.rb +113 -0
  115. data/lib/conceptql/utils_cli.rb +17 -0
  116. data/lib/conceptql/version.rb +3 -1
  117. data/lib/conceptql/visitors/metadata_extractor.rb +54 -0
  118. data/lib/conceptql/vocabularies/behaviors/costish.rb +24 -0
  119. data/lib/conceptql/vocabularies/behaviors/gdmish.rb +19 -0
  120. data/lib/conceptql/vocabularies/behaviors/omopish.rb +32 -0
  121. data/lib/conceptql/vocabularies/dynamic_vocabularies.rb +63 -0
  122. data/lib/conceptql/vocabularies/entry.rb +138 -0
  123. data/lib/conceptql/vocabularies/vocabulary_class_factory.rb +85 -0
  124. data/lib/conceptql/window/base.rb +26 -0
  125. data/lib/conceptql/window/date_range.rb +25 -0
  126. data/lib/conceptql/window/table.rb +116 -0
  127. data/lib/conceptql/window.rb +72 -0
  128. data/lib/conceptql.rb +65 -2
  129. data/lib/sequel/extensions/pg_ctas_explain.rb +22 -0
  130. data/lib/sequel/extensions/pg_vacuum_table.rb +23 -0
  131. data/schemas/gdm.yml +882 -0
  132. data/schemas/gdm_wide.yml +757 -0
  133. data/schemas/ohdsi_vocabs.yml +143 -0
  134. data/schemas/omopv4_plus.yml +729 -0
  135. data/schemas/omopv4_plus_more_ignorables.tsv +13 -0
  136. metadata +267 -207
  137. data/.gitignore +0 -23
  138. data/Gemfile +0 -4
  139. data/Guardfile +0 -56
  140. data/LICENSE.txt +0 -22
  141. data/Rakefile +0 -1
  142. data/conceptql.gemspec +0 -32
  143. data/doc/ConceptQL Specification (alpha).pdf +0 -0
  144. data/doc/diagram_0.png +0 -0
  145. data/doc/implementation_notes.md +0 -39
  146. data/doc/spec.md +0 -1344
  147. data/lib/conceptql/behaviors/debuggable.rb +0 -70
  148. data/lib/conceptql/behaviors/dottable.rb +0 -103
  149. data/lib/conceptql/behaviors/preppable.rb +0 -20
  150. data/lib/conceptql/converter.rb +0 -65
  151. data/lib/conceptql/debugger.rb +0 -48
  152. data/lib/conceptql/fake_grapher.rb +0 -20
  153. data/lib/conceptql/graph.rb +0 -52
  154. data/lib/conceptql/graph_nodifier.rb +0 -203
  155. data/lib/conceptql/logger.rb +0 -10
  156. data/lib/conceptql/operators/complement.rb +0 -38
  157. data/lib/conceptql/operators/concept.rb +0 -70
  158. data/lib/conceptql/operators/condition_type.rb +0 -71
  159. data/lib/conceptql/operators/cpt.rb +0 -25
  160. data/lib/conceptql/operators/drug_type_concept.rb +0 -21
  161. data/lib/conceptql/operators/from_seer_visits.rb +0 -23
  162. data/lib/conceptql/operators/hcpcs.rb +0 -25
  163. data/lib/conceptql/operators/icd10.rb +0 -28
  164. data/lib/conceptql/operators/icd9.rb +0 -28
  165. data/lib/conceptql/operators/icd9_procedure.rb +0 -25
  166. data/lib/conceptql/operators/intersect.rb +0 -34
  167. data/lib/conceptql/operators/loinc.rb +0 -25
  168. data/lib/conceptql/operators/medcode.rb +0 -28
  169. data/lib/conceptql/operators/medcode_procedure.rb +0 -27
  170. data/lib/conceptql/operators/ndc.rb +0 -29
  171. data/lib/conceptql/operators/observation_by_enttype.rb +0 -29
  172. data/lib/conceptql/operators/observation_period.rb +0 -30
  173. data/lib/conceptql/operators/overlapped_by.rb +0 -23
  174. data/lib/conceptql/operators/overlaps.rb +0 -19
  175. data/lib/conceptql/operators/place_of_service_code.rb +0 -24
  176. data/lib/conceptql/operators/procedure_occurrence.rb +0 -25
  177. data/lib/conceptql/operators/prodcode.rb +0 -29
  178. data/lib/conceptql/operators/rxnorm.rb +0 -24
  179. data/lib/conceptql/operators/snomed.rb +0 -24
  180. data/lib/conceptql/operators/snomed_condition.rb +0 -26
  181. data/lib/conceptql/operators/source_vocabulary_operator.rb +0 -58
  182. data/lib/conceptql/operators/standard_vocabulary_operator.rb +0 -45
  183. data/lib/conceptql/operators/started_by.rb +0 -22
  184. data/lib/conceptql/operators/to_seer_visits.rb +0 -24
  185. data/lib/conceptql/operators/visit.rb +0 -15
  186. data/lib/conceptql/operators/visit_occurrence.rb +0 -30
  187. data/lib/conceptql/tree.rb +0 -53
  188. data/lib/conceptql/utils/temp_table.rb +0 -72
  189. data/spec/conceptql/behaviors/dottable_spec.rb +0 -99
  190. data/spec/conceptql/converter_spec.rb +0 -51
  191. data/spec/conceptql/date_adjuster_spec.rb +0 -68
  192. data/spec/conceptql/operators/after_spec.rb +0 -16
  193. data/spec/conceptql/operators/before_spec.rb +0 -16
  194. data/spec/conceptql/operators/casting_operator_spec.rb +0 -68
  195. data/spec/conceptql/operators/complement_spec.rb +0 -15
  196. data/spec/conceptql/operators/concept_spec.rb +0 -40
  197. data/spec/conceptql/operators/condition_type_spec.rb +0 -128
  198. data/spec/conceptql/operators/contains_spec.rb +0 -19
  199. data/spec/conceptql/operators/cpt_spec.rb +0 -29
  200. data/spec/conceptql/operators/date_range_spec.rb +0 -33
  201. data/spec/conceptql/operators/death_spec.rb +0 -10
  202. data/spec/conceptql/operators/during_spec.rb +0 -30
  203. data/spec/conceptql/operators/except_spec.rb +0 -15
  204. data/spec/conceptql/operators/first_spec.rb +0 -35
  205. data/spec/conceptql/operators/from_spec.rb +0 -13
  206. data/spec/conceptql/operators/gender_spec.rb +0 -27
  207. data/spec/conceptql/operators/hcpcs_spec.rb +0 -29
  208. data/spec/conceptql/operators/icd10_spec.rb +0 -34
  209. data/spec/conceptql/operators/icd9_procedure_spec.rb +0 -29
  210. data/spec/conceptql/operators/icd9_spec.rb +0 -34
  211. data/spec/conceptql/operators/intersect_spec.rb +0 -28
  212. data/spec/conceptql/operators/last_spec.rb +0 -36
  213. data/spec/conceptql/operators/loinc_spec.rb +0 -29
  214. data/spec/conceptql/operators/medcode_procedure_spec.rb +0 -34
  215. data/spec/conceptql/operators/medcode_spec.rb +0 -34
  216. data/spec/conceptql/operators/observation_period_spec.rb +0 -10
  217. data/spec/conceptql/operators/occurrence_spec.rb +0 -87
  218. data/spec/conceptql/operators/overlapped_by_spec.rb +0 -32
  219. data/spec/conceptql/operators/overlaps_spec.rb +0 -21
  220. data/spec/conceptql/operators/person_filter_spec.rb +0 -15
  221. data/spec/conceptql/operators/person_spec.rb +0 -10
  222. data/spec/conceptql/operators/place_of_service_code_spec.rb +0 -24
  223. data/spec/conceptql/operators/procedure_occurrence_spec.rb +0 -10
  224. data/spec/conceptql/operators/prodcode_spec.rb +0 -35
  225. data/spec/conceptql/operators/query_double.rb +0 -20
  226. data/spec/conceptql/operators/query_double_spec.rb +0 -7
  227. data/spec/conceptql/operators/race_spec.rb +0 -21
  228. data/spec/conceptql/operators/rxnorm_spec.rb +0 -29
  229. data/spec/conceptql/operators/snomed_spec.rb +0 -29
  230. data/spec/conceptql/operators/source_vocabulary_operator_spec.rb +0 -35
  231. data/spec/conceptql/operators/standard_vocabulary_operator_spec.rb +0 -35
  232. data/spec/conceptql/operators/started_by_spec.rb +0 -22
  233. data/spec/conceptql/operators/temporal_operator_spec.rb +0 -51
  234. data/spec/conceptql/operators/time_window_spec.rb +0 -77
  235. data/spec/conceptql/operators/union_spec.rb +0 -21
  236. data/spec/conceptql/operators/visit_occurrence_spec.rb +0 -10
  237. data/spec/conceptql/query_spec.rb +0 -22
  238. data/spec/conceptql/tree_spec.rb +0 -50
  239. data/spec/doubles/stream_for_casting_double.rb +0 -9
  240. data/spec/doubles/stream_for_occurrence_double.rb +0 -25
  241. data/spec/doubles/stream_for_temporal_double.rb +0 -6
  242. 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
@@ -1,36 +1,77 @@
1
- require 'facets/hash/deep_rekey'
2
- require 'facets/pathname/chdir'
3
- require 'facets/string/modulize'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "operators/operator"
4
4
 
5
5
  module ConceptQL
6
6
  class Nodifier
7
- attr_reader :operators
8
-
9
- def initialize
10
- @operators = {}
11
- dir = Pathname.new(__FILE__).dirname()
12
- dir.chdir do
13
- Pathname.glob("operators/*.rb").each do |file|
14
- require_relative file
15
- operator = file.basename('.*').to_s.to_sym
16
- klass = Object.const_get("conceptQL/operators/#{operator}".modulize)
17
- @operators[operator] = klass
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 create(scope, operator, *values)
23
- operator = operator.to_sym
24
- if operators[operator].nil?
25
- raise "Can't find operator for '#{operator}' in #{operators.keys.sort}"
26
- end
27
- operator = operators[operator].new(*values)
28
- operator.scope = scope
29
- operator
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 to_metadata
33
- Hash[operators.map { |k, v| [k, v.to_metadata]}.select { |k, v| v[:desc] }]
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
- require_relative 'temporal_operator'
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
- desc <<-EOF
7
- Compares all results on a person-by-person basis between the left hand results (LHR) and the right hand resuls (RHR).
8
- For any result in the LHR whose start_date occurs after the earliest end_date of the RHR, that result is passed through.
9
- All other results are discarded, including all results in the RHR.
10
- L-------N-------L
11
- R-----R
12
- R-----R
13
- L-----Y----L
14
- EOF
15
- def right_stream(db)
16
- right.evaluate(db).from_self.group_by(:person_id).select(:person_id, Sequel.function(:min, :end_date).as(:end_date)).as(:r)
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
- Proc.new { l__start_date > r__end_date }
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
- require_relative 'temporal_operator'
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
- desc 'If a result from the LHR overlaps in any way a result from the RHR it is passed along.'
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
- l_partly_in_r = Sequel.expr { r__start_date <= l__start_date }.&(Sequel.expr { l__start_date <= r__end_date })
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
- require_relative 'temporal_operator'
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
- desc <<-EOF
7
- Compares all results on a person-by-person basis between the left hand results (LHR) and the right hand resuls (RHR).
8
- For any result in the LHR whose end_date occurs before the most recent start_date of the RHR, that result is passed through.
9
- All other results are discarded, including all results in the RHR.
10
- EOF
11
-
12
- def right_stream(db)
13
- right.evaluate(db).from_self.group_by(:person_id).select(:person_id, Sequel.function(:max, :start_date).as(:start_date)).as(:r)
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
- Proc.new { l__end_date < r__start_date }
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
- require_relative 'operator'
2
- require 'facets/string/titlecase'
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
- def graph_it(g, db)
16
- left.graph_it(g, db)
17
- right.graph_it(g, db)
18
- cluster_name = "cluster_#{operator_name}"
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 display_name
30
- self.class.name.split('::').last.snakecase.titlecase
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
- def left
35
- @left ||= options[:left]
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 right
39
- @right ||= options[:right]
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
-