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
@@ -1,97 +1,250 @@
1
- require 'facets/kernel/meta_def'
2
- require 'facets/string/snakecase'
1
+ # frozen_string_literal: true
3
2
 
4
- module Metadatable
5
- def preferred_name(value = nil)
6
- return @preferred_name unless value
7
- @preferred_name = value
8
- end
3
+ module ConceptQL
4
+ module Metadatable
5
+ def preferred_name(value = nil)
6
+ return @preferred_name unless value
9
7
 
10
- def desc(value = nil)
11
- return @desc unless value
12
- @desc = value
13
- end
8
+ @preferred_name = value
9
+ end
14
10
 
15
- def predominant_types(*values)
16
- return @predominant_types if values.empty?
17
- @predominant_types = values
18
- end
11
+ def aliases(value = nil)
12
+ return @aliases unless value
19
13
 
20
- def argument(name, options = {})
21
- (@arguments ||= [])
22
- @arguments << [name, options]
23
- end
14
+ @aliases = value
15
+ end
24
16
 
25
- def option(name, options = {})
26
- @options ||= {}
27
- @options[name] = options
28
- end
17
+ def desc(value = nil)
18
+ return @desc unless value
19
+
20
+ @desc = value
21
+ end
22
+
23
+ def short_name(value = nil)
24
+ return @short_name unless value
25
+
26
+ @short_name = value
27
+ end
28
+
29
+ def long_name(value = nil)
30
+ return @long_name unless value
31
+
32
+ @long_name = value
33
+ end
34
+
35
+ def conceptql_spec_id(value = nil)
36
+ return @conceptql_spec_id unless value
37
+
38
+ @conceptql_spec_id = value
39
+ end
40
+
41
+ def predominant_domains(*values)
42
+ return @predominant_domains if values.empty?
43
+
44
+ @predominant_domains = values
45
+ end
46
+
47
+ def argument(name, options = {})
48
+ (@arguments ||= [])
49
+ @arguments << [ name, auto_label(name, options) ]
50
+ end
51
+
52
+ def option(name, options = {})
53
+ @options ||= {}
54
+ @options[name] = auto_label(name, options)
55
+ end
56
+
57
+ # Takes an option that a subclass inherited out of its metadata
58
+ def remove_option(name)
59
+ @options.delete(name)
60
+ end
61
+
62
+ def auto_label(name, opts = {})
63
+ return opts if opts[:label]
64
+ return opts.merge(label: name.to_s.split("_").map(&:capitalize).join(" ")) unless opts[:type] == :codelist
29
65
 
30
- def types(*type_list)
31
- @types = type_list
32
- define_method(:types) do
33
- type_list
66
+ opts.merge(label: "#{pref_name} Codes")
34
67
  end
35
- if type_list.length == 1
36
- define_method(:type) do
37
- type_list.first
68
+
69
+ def domains(*domain_list)
70
+ @domains = domain_list
71
+ define_method(:domains) do |_db|
72
+ domain_list
73
+ end
74
+ return unless domain_list.length == 1
75
+
76
+ define_method(:domain) do
77
+ domain_list.first
38
78
  end
39
79
  end
40
- end
41
80
 
42
- def allows_many_upstreams
43
- @max_upstreams = 99
44
- end
81
+ def basic_type(value = nil)
82
+ return @basic_type unless value
45
83
 
46
- def allows_one_upstream
47
- @max_upstreams = 1
48
- end
84
+ @basic_type = value
85
+ end
49
86
 
50
- def just_class_name
51
- self.to_s.split('::').last
52
- end
87
+ def allows_many_upstreams
88
+ @max_upstreams = 99
89
+ end
53
90
 
54
- def humanized_class_name
55
- just_class_name.gsub(/([A-Z])/, ' \1').lstrip
56
- end
91
+ def allows_one_upstream
92
+ @max_upstreams = 1
93
+ end
57
94
 
58
- def category(category)
59
- (@categories ||= [])
60
- @categories << Array(category)
61
- end
95
+ def just_class_name
96
+ to_s.split("::").last
97
+ end
62
98
 
63
- def reset_categories
64
- @categories = []
65
- end
99
+ def humanized_class_name
100
+ just_class_name.gsub(/([A-Z])/, ' \1').lstrip
101
+ end
66
102
 
67
- def inherited(upstream)
68
- (@options || {}).each do |name, opt|
69
- upstream.option name, opt
103
+ def category(category)
104
+ (@categories ||= [])
105
+ @categories << Array(category)
70
106
  end
71
107
 
72
- (@categories || []).each do |cat|
73
- upstream.category cat
108
+ def reset_categories
109
+ @categories = []
74
110
  end
75
111
 
76
- case @max_upstreams
77
- when 1
78
- upstream.allows_one_upstream
79
- when 99
80
- upstream.allows_many_upstreams
112
+ def inherited(upstream)
113
+ super
114
+ (@options || {}).each do |name, opt|
115
+ upstream.option name, opt
116
+ end
117
+
118
+ (@categories || []).each do |cat|
119
+ upstream.category cat
120
+ end
121
+
122
+ upstream.basic_type @basic_type
123
+
124
+ case @max_upstreams
125
+ when 1
126
+ upstream.allows_one_upstream
127
+ when 99
128
+ upstream.allows_many_upstreams
129
+ end
130
+ end
131
+
132
+ def pref_name
133
+ @preferred_name || humanized_class_name
134
+ end
135
+
136
+ def spec_id
137
+ "#{@conceptql_spec_id || pref_name} operator".gsub(/\s+/, "-").downcase
138
+ end
139
+
140
+ def to_metadata(name, opts = {})
141
+ derive_metadata_from_validations
142
+ warn_about_missing_metadata if opts[:warn]
143
+ {
144
+ name: name,
145
+ preferred_name: pref_name,
146
+ short_name: short_name,
147
+ long_name: long_name,
148
+ operation: just_class_name.underscore,
149
+ min_upstreams: @max_upstreams || 0,
150
+ max_upstreams: @max_upstreams || 0,
151
+ arguments: @arguments || [],
152
+ options: process_options(@options),
153
+ predominant_domains: @domains || @predominant_domains || [],
154
+ desc: get_desc,
155
+ categories: @categories || [],
156
+ basic_type: @basic_type,
157
+ deprecated: nil, # kept for metadata consumers; no operator is deprecated
158
+ arity: arity,
159
+ aliases: @aliases,
160
+ spec_id: spec_id
161
+ }
162
+ end
163
+
164
+ def arity
165
+ case basic_type
166
+ when :set
167
+ -1
168
+ when :temporal, :cast
169
+ 1
170
+ when :selection, nil
171
+ 0
172
+ when :filter
173
+ 2
174
+ else
175
+ raise "#{name} has unknown basic_type: #{basic_type.pretty_inspect}"
176
+ end
177
+ end
178
+
179
+ def get_desc
180
+ @get_desc ||= standard_description
81
181
  end
82
- end
83
182
 
84
- def to_metadata
85
- {
86
- preferred_name: @preferred_name || humanized_class_name,
87
- operation: just_class_name.snakecase,
88
- max_upstreams: @max_upstreams || 0,
89
- arguments: @arguments || [],
90
- options: @options || {},
91
- predominant_types: @types || @predominant_types || [],
92
- desc: @desc,
93
- categories: @categories || []
94
- }
183
+ def process_options(options)
184
+ return {} if options.nil? || options.empty?
185
+
186
+ # Build new hashes: the option hashes can be shared with a parent class
187
+ options.transform_values do |v|
188
+ v[:options].respond_to?(:call) ? v.merge(options: v[:options].call) : v
189
+ end
190
+ end
191
+
192
+ def warn_about_missing_metadata
193
+ missing = []
194
+ missing << :categories if (@categories || []).empty?
195
+ missing << :desc if get_desc.empty?
196
+ missing << :basic_type unless @basic_type
197
+ puts "#{just_class_name} is missing #{missing.join(', ')}" unless missing.empty?
198
+ end
199
+
200
+ # Each validation is [meth, *args]. Every argument reaches the hook, so
201
+ # validate_required_options :left, :right marks both options.
202
+ def derive_metadata_from_validations
203
+ @validations.each do |meth, *args|
204
+ hook = "#{meth}_to_metadata"
205
+ send(hook, *args) if respond_to?(hook)
206
+ end
207
+ end
208
+
209
+ def validate_no_upstreams_to_metadata(*_args)
210
+ @min_upstreams = 0
211
+ @max_upstreams = 0
212
+ end
213
+
214
+ def validate_one_upstream_to_metadata(*_args)
215
+ @min_upstreams = 1
216
+ @max_upstreams = 1
217
+ end
218
+
219
+ def validate_at_least_one_upstream_to_metadata(*_args)
220
+ @min_upstreams = 1
221
+ @max_upstreams = 99
222
+ end
223
+
224
+ def validate_at_most_one_upstream_to_metadata(*_args)
225
+ @min_upstreams = 0
226
+ @max_upstreams = 1
227
+ end
228
+
229
+ def validate_no_arguments_to_metadata(*_args)
230
+ @arguments = []
231
+ end
232
+
233
+ # A subclass shares the option hashes that it inherits (see #inherited),
234
+ # so mark a copy. Writing into the shared hash would mark the option as
235
+ # required for the parent class too.
236
+ def validate_required_options_to_metadata(*names)
237
+ names.each do |name|
238
+ @options[name] = @options.fetch(name).merge(required: true)
239
+ end
240
+ end
241
+
242
+ def standard_description
243
+ "Selects records based on the #{pref_name} vocabulary."
244
+ end
245
+
246
+ def no_desc
247
+ desc ""
248
+ end
95
249
  end
96
250
  end
97
-
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConceptQL
4
+ module Provenanceable
5
+ def self.included(base)
6
+ base.require_column :file_provenance_type
7
+ base.require_column :code_provenance_type
8
+ end
9
+
10
+ def prov_of(db, ancestors)
11
+ dm.descendants_of(db,
12
+ dm.concept_ids(db, [ dm.file_provenance_types_vocab, dm.code_provenance_types_vocab ].flatten,
13
+ ancestors))
14
+ end
15
+
16
+ def build_where_from_codes(db, codes)
17
+ Sequel.|(
18
+ { file_provenance_type: prov_of(db, codes) }, { code_provenance_type: prov_of(db, codes) }
19
+ )
20
+ end
21
+
22
+ def find_bad_keywords(db, codes)
23
+ codes - dm.concept_codes(db, [ dm.file_provenance_types_vocab, dm.code_provenance_types_vocab ].flatten)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConceptQL
4
+ module Behaviors
5
+ module SingleEvent
6
+ def events_per_patient
7
+ :single
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConceptQL
4
+ module Behaviors
5
+ module Timeless
6
+ def timeless?
7
+ true
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,206 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module ConceptQL
6
+ module Behaviors
7
+ module Utilizable
8
+ extend ActiveSupport::Concern
9
+ include ConceptQL::Behaviors::Windowable
10
+
11
+ included do
12
+ domains :condition_occurrence
13
+ category "Select by Property"
14
+ basic_type :selection
15
+ validate_no_upstreams
16
+ validate_no_arguments
17
+
18
+ require_column :admission_date
19
+ require_column :discharge_date
20
+ require_column :length_of_stay
21
+ require_column :admission_source_value
22
+ require_column :admission_source_description
23
+ require_column :discharge_location_source_value
24
+ require_column :discharge_location_source_description
25
+ require_column :primary_concept_id
26
+ end
27
+
28
+ def column_family
29
+ "inpatient"
30
+ end
31
+
32
+ # The operators that include this register for gdm and gdm_wide only.
33
+ def query(db)
34
+ gdm_it(db)
35
+ end
36
+
37
+ def collection_type
38
+ raise NotImplementedError
39
+ end
40
+
41
+ # With the Database's share_utilization option, the list is read from a
42
+ # table that every statement on the Database shares. The WITH entry only
43
+ # carries the list to Scope#with_ctes, which takes it out of the SQL and
44
+ # creates the table when the query first reads rows.
45
+ def evaluate(db)
46
+ cdb = nodifier.cdb
47
+ return super unless cdb.opts[:share_utilization]
48
+
49
+ list = select_it(query(db))
50
+ name = cdb.shared_utilization_table(list, scope)
51
+ db[name].with(name, list, Scope::SHARED_TABLE => true)
52
+ end
53
+
54
+ def gdm_it(db)
55
+ source_type_id = dm.concept_ids(db, dm.file_provenance_types_vocab, collection_type)
56
+ all_source_type_ids = dm.descendants_of(db, source_type_id)
57
+ primary_id = dm.concept_ids(db, dm.code_provenance_types_vocab, "primary")
58
+ all_primary_ids = dm.descendants_of(db, primary_id)
59
+ # condition_vocabularies = dm.vocabularies_query.where(domain: 'condition_occurrence').select_map(:id)
60
+
61
+ return gdm_wide_it(db, all_primary_ids, all_source_type_ids) if wide?
62
+
63
+ # Get primary diagnosis codes
64
+ primary_concepts = rdbms.primary_concepts(db, all_primary_ids)
65
+
66
+ relevant_contexts = db[Sequel[:contexts].as(:cn)]
67
+ .where(Sequel[:cn][:source_type_concept_id] => all_source_type_ids)
68
+ .select(:collection_id)
69
+
70
+ db[:collections].from_self(alias: :cl)
71
+ .then { |cl| restrict_to_window_persons(cl, Sequel[:cl][:patient_id]) }
72
+ .join(:admission_details, { Sequel[:ad][:id] => Sequel[:cl][:admission_detail_id] }, table_alias: :ad)
73
+ .left_join(dm.name_concepts_table(db), { Sequel[:ad][:admit_source_concept_id] => Sequel[:asc][:id] }, table_alias: :asc)
74
+ .left_join(dm.name_concepts_table(db), { Sequel[:ad][:discharge_location_concept_id] => Sequel[:dlc][:id] }, table_alias: :dlc)
75
+ .left_join(primary_concepts, { Sequel[:pcon][:collection_id] => Sequel[:cl][:id] }, table_alias: :pcon)
76
+ .where(Sequel[:cl][:id] => relevant_contexts)
77
+ .select(
78
+ Sequel[:cl][:start_date].as(:collection_start_date),
79
+ Sequel[:cl][:end_date].as(:collection_end_date),
80
+ Sequel[:ad][:admission_date].as(:admission_date),
81
+ Sequel[:ad][:discharge_date].as(:discharge_date),
82
+ Sequel[:asc][:concept_code].as(:admission_source_value),
83
+ Sequel[:asc][:concept_text].as(:admission_source_description),
84
+ Sequel[:dlc][:concept_code].as(:discharge_location_source_value),
85
+ Sequel[:dlc][:concept_text].as(:discharge_location_source_description),
86
+ Sequel[:pcon][:concept_id].as(:primary_concept_id),
87
+ Sequel[:pcon][:concept_code].as(:source_value),
88
+ Sequel[:pcon][:vocabulary_id].as(:source_vocabulary_id),
89
+ Sequel[:cl][:patient_id].as(:patient_id),
90
+ Sequel[:cl][:id].as(:collection_id)
91
+ )
92
+ .select_append(Sequel[:ROW_NUMBER].function.over(
93
+ partition: [ :admission_detail_id ],
94
+ order: [
95
+ Sequel[:pcon][:is_primary].desc,
96
+ Sequel[:cl][:id],
97
+ Sequel[:pcon][:concept_code]
98
+ ]
99
+ ).as(:nummy))
100
+ .from_self
101
+ .where(nummy: 1)
102
+ end
103
+
104
+ def gdm_wide_it(db, all_primary_ids, all_source_type_ids)
105
+ # Emit `CASE WHEN provenance_concept_id IN (...) THEN 1 ELSE 0 END`.
106
+ # The previous multi-WHEN form (`Sequel.case({{prov=>id1}=>1, {prov=>id2}=>1, ...}, 0)`)
107
+ # is mis-evaluated by Spark 3.3.2 when its result feeds the outer
108
+ # `CASE is_primary WHEN 1 THEN col ELSE NULL END` below. See conceptql-pk7.
109
+ is_primary =
110
+ if all_primary_ids.empty?
111
+ Sequel[0]
112
+ else
113
+ Sequel.case(
114
+ [ [ { Sequel[:provenance_concept_id] => all_primary_ids }, 1 ] ],
115
+ 0
116
+ )
117
+ end
118
+
119
+ db[:observations]
120
+ .then { |observations| restrict_to_window_persons(observations, :patient_id) }
121
+ .where(source_type_concept_id: all_source_type_ids)
122
+ .exclude(admit_admission_date: nil)
123
+ .select_append(
124
+ is_primary.as(:is_primary)
125
+ )
126
+ .from_self
127
+ # One admission has many rows with the same concept on different
128
+ # line dates. The unique id ends the ORDER BY so the kept row is the
129
+ # same on every run, and admission_end_date takes the latest end_date
130
+ # over all of the admission's rows, not the kept row's. See conceptql-evk.
131
+ .select_append(
132
+ Sequel[:ROW_NUMBER].function.over(
133
+ partition: [ :admission_detail_id ],
134
+ order: [ Sequel[:is_primary].desc, Sequel[:collection_id], Sequel[:clinical_code_concept_id], Sequel[:id] ]
135
+ ).as(:nummy),
136
+ Sequel.function(:max, :end_date).over(partition: [ :admission_detail_id ]).as(:admission_end_date)
137
+ )
138
+ .from_self
139
+ .where(nummy: 1)
140
+ .from_self
141
+ .left_join(dm.name_concepts_table(db), { Sequel[:admit_source_concept_id] => Sequel[:asc][:id] }, table_alias: :asc)
142
+ .left_join(dm.name_concepts_table(db), { Sequel[:discharge_location_concept_id] => Sequel[:dlc][:id] }, table_alias: :dlc)
143
+ .select_append(
144
+ Sequel[:asc][:concept_code].as(:admission_source_value),
145
+ Sequel[:asc][:concept_text].as(:admission_source_description),
146
+ Sequel[:dlc][:concept_code].as(:discharge_location_source_value),
147
+ Sequel[:dlc][:concept_text].as(:discharge_location_source_description),
148
+ Sequel.case({ 1 => :clinical_code_concept_id }, nil, :is_primary).as(:primary_concept_id),
149
+ Sequel.case({ 1 => :clinical_code_source_value }, nil, :is_primary).as(:source_value),
150
+ Sequel.case({ 1 => :clinical_code_vocabulary_id }, nil, :is_primary).as(:source_vocabulary_id)
151
+ )
152
+ .from_self
153
+ end
154
+
155
+ # Keep only the persons that the scope's table windows cover, before the
156
+ # ROW_NUMBER over admission_detail_id. Without this, Spark numbers every
157
+ # inpatient row in the database and applies the window join afterwards.
158
+ # Output is unchanged because one admission never belongs to two persons
159
+ # (Ryan, 2026-10-14), so each ROW_NUMBER partition is kept or dropped whole.
160
+ def restrict_to_window_persons(ds, person_column)
161
+ return ds if skip_windows?
162
+
163
+ scope.windows.grep(Window::Table).inject(ds) do |restricted, window|
164
+ restricted.where(person_column => window.person_ids(restricted.db))
165
+ end
166
+ end
167
+
168
+ def table
169
+ :collections
170
+ end
171
+
172
+ def override_columns
173
+ return unless gdm?
174
+
175
+ {
176
+ length_of_stay: (rdbms.days_between(Sequel[:admission_date],
177
+ discharge_date_column) + 1).as(:length_of_stay),
178
+ criterion_table: Sequel.cast_string("collections").as(:criterion_table),
179
+ criterion_domain: Sequel.cast_string("condition_occurrence").as(:criterion_domain),
180
+ admission_source_value: Sequel[:admission_source_value],
181
+ admission_source_description: Sequel[:admission_source_description],
182
+ discharge_location_source_value: Sequel[:discharge_location_source_value],
183
+ discharge_location_source_description: Sequel[:discharge_location_source_description],
184
+ admission_date: Sequel[:admission_date],
185
+ discharge_date: discharge_date_column.as(:discharge_date),
186
+ person_id: Sequel[:patient_id].as(:person_id),
187
+ criterion_id: Sequel[:collection_id].as(:criterion_id),
188
+ start_date: Sequel[:collection_start_date].as(:start_date),
189
+ end_date: Sequel[:collection_end_date].as(:end_date),
190
+ primary_concept_id: Sequel[:primary_concept_id],
191
+ source_value: Sequel[:source_value].as(:source_value),
192
+ source_vocabulary_id: Sequel[:source_vocabulary_id].as(:source_vocabulary_id)
193
+ }
194
+ end
195
+
196
+ # gdm_wide's AdmissionDateQueryModifier falls back to the kept row's own
197
+ # end_date when the admission has no discharge date. An admission falls
198
+ # back to its latest end_date instead (Ryan, 2026-10-16). See conceptql-evk.
199
+ def discharge_date_column
200
+ return Sequel[:discharge_date] unless wide?
201
+
202
+ Sequel.function(:coalesce, :admit_discharge_date, :admission_end_date, :discharge_date)
203
+ end
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConceptQL
4
+ module Behaviors
5
+ # Applies scope windows to Operators
6
+ module Windowable
7
+ def select_it(query, opts = {})
8
+ ds = super
9
+ return ds if skip_windows?
10
+
11
+ scope.windows.each do |window|
12
+ ds = window.call(self, ds, timeless: timeless?)
13
+ end
14
+
15
+ ds
16
+ end
17
+
18
+ def skip_windows?
19
+ false
20
+ end
21
+
22
+ def timeless?
23
+ false
24
+ end
25
+ end
26
+ end
27
+ end