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,235 +1,681 @@
1
- require 'zlib'
2
- require_relative '../behaviors/metadatable'
3
- require 'facets/array/extract_options'
4
- require 'facets/hash/deep_rekey'
5
- require 'forwardable'
1
+ # frozen_string_literal: true
2
+
3
+ require "zlib"
4
+ require_relative "../behaviors/metadatable"
5
+ require "forwardable"
6
6
 
7
7
  module ConceptQL
8
8
  module Operators
9
+ # Built-in operators only: the classes that call Operator.register when
10
+ # their file loads. Vocabulary operators are not in here; each
11
+ # ConceptQL::Database builds its own from its lexicon (Database#operators).
12
+ STATIC_OPERATORS = { omopv4_plus: {}, gdm: {}, gdm_wide: {} }.freeze
13
+
14
+ SELECTED_COLUMNS = %i[person_id
15
+ criterion_id
16
+ criterion_table
17
+ criterion_domain
18
+ start_date
19
+ end_date
20
+ value_as_number
21
+ value_as_string
22
+ value_as_concept_id
23
+ units_source_value
24
+ source_value].freeze
25
+
26
+ def self.static_operators
27
+ STATIC_OPERATORS
28
+ end
29
+
9
30
  class Operator
10
31
  extend Forwardable
11
- extend Metadatable
12
- COLUMNS = [
13
- :person_id,
14
- :criterion_id,
15
- :criterion_type,
16
- :start_date,
17
- :end_date,
18
- :value_as_number,
19
- :value_as_string,
20
- :value_as_concept_id,
21
- :units_source_value,
22
- :source_value
23
- ]
24
-
25
- attr :values, :options, :arguments, :upstreams
32
+ extend ConceptQL::Metadatable
33
+
34
+ attr_reader :nodifier, :values, :options, :arguments, :upstreams, :op_name, :errors, :warnings
26
35
 
27
36
  option :label, type: :string
28
37
 
29
- def initialize(*args)
30
- set_values(*args)
31
- end
38
+ @validations = []
39
+
40
+ class << self
41
+ attr_reader :validations, :required_columns
42
+
43
+ def register(file, *data_models)
44
+ data_models = STATIC_OPERATORS.keys if data_models.empty?
45
+ data_models.each do |dm|
46
+ op_name = File.basename(file).sub(/\.rb\z/, "").downcase
47
+ Operators.static_operators[dm][op_name] = self
48
+ end
49
+ end
50
+
51
+ def query_columns(*tables)
52
+ define_method(:query_cols) do
53
+ dm.table_columns(*tables)
54
+ end
55
+ end
56
+
57
+ def default_query_columns
58
+ define_method(:query_cols) do
59
+ dynamic_columns
60
+ end
61
+ end
32
62
 
33
- def values=(*args)
34
- set_values(*args)
63
+ def require_column(column)
64
+ @required_columns ||= []
65
+ @required_columns << column
66
+ end
67
+
68
+ validation_meths = <<-END.split.map(&:to_sym)
69
+ no_upstreams
70
+ one_upstream
71
+ at_least_one_upstream
72
+ at_most_one_upstream
73
+ no_arguments
74
+ one_argument
75
+ at_least_one_argument
76
+ at_most_one_argument
77
+ option
78
+ required_options
79
+ codes_match
80
+ END
81
+
82
+ validation_meths.each do |type|
83
+ meth = :"validate_#{type}"
84
+ define_method(meth) do |*args|
85
+ validations << [ meth, *args ]
86
+ end
87
+ end
88
+
89
+ # Declares inherited options that this operator does not support.
90
+ # They leave the operator's metadata, and a statement that sets one
91
+ # fails validation.
92
+ def unsupported_options(*names)
93
+ names.each { |name| remove_option(name) }
94
+ validations << [ :validate_unsupported_options, *names ]
95
+ end
96
+
97
+ def inherited(subclass)
98
+ super
99
+ subclass.instance_variable_set(:@validations, validations.dup)
100
+ end
101
+
102
+ def new(*)
103
+ operator = super
104
+
105
+ # If operator has a label, replace it with a recall so all references
106
+ # to it use the same code.
107
+ if operator.label && !operator.errors
108
+ operator.scope.add_operator(operator)
109
+ operator = Operators::Recall.new(operator.nodifier, "recall", operator.label, replaced: true)
110
+ end
111
+
112
+ operator
113
+ end
35
114
  end
36
115
 
37
- def set_values(*args)
38
- @options = args.extract_options!.deep_rekey
39
- @upstreams, @arguments = args.partition { |arg| arg.is_a?(Operator) }
116
+ def initialize(nodifier, op_name, *args)
117
+ @nodifier = nodifier
118
+
119
+ # Under what name was this operator instantiated?
120
+ # For operators like "vocabulary", this tells the instance which
121
+ # vocabulary operator to impersonate
122
+ @op_name = op_name
123
+ @options = {}
124
+ @options = @options.merge(ConceptQL::Utils.rekey(args.pop)) while args.last.is_a?(Hash)
125
+ args.reject! { |arg| arg.nil? || arg == "" }
126
+ @upstreams, @arguments = args.partition { |arg| arg.is_a?(Array) || arg.is_a?(Operator) }
40
127
  @values = args
128
+
129
+ scope.nest(self) do
130
+ create_upstreams
131
+ end
132
+ end
133
+
134
+ def create_upstreams
135
+ @upstreams.map! { |stmt| to_op(stmt) }
136
+ end
137
+
138
+ def to_op(stmt)
139
+ stmt.is_a?(Operator) ? stmt : nodifier.create(*stmt)
140
+ end
141
+
142
+ def operator_name
143
+ self.class.just_class_name.underscore
144
+ end
145
+
146
+ def required_columns
147
+ cols = self.class.required_columns || []
148
+ cols |= [ :uuid ] if options[:uuid]
149
+ cols
150
+ end
151
+
152
+ def dynamic_columns
153
+ scope.query_columns
154
+ end
155
+
156
+ def cte_name(name)
157
+ scope.cte_name(name)
158
+ end
159
+
160
+ def annotate(db, opts = {})
161
+ return @annotate if defined?(@annotate)
162
+
163
+ scope_key = options[:id] || op_name
164
+ annotation = {}
165
+ counts = (annotation[:counts] ||= {})
166
+ metadata = { annotation: annotation }
167
+
168
+ if (name = preferred_name)
169
+ metadata[:name] = name
170
+ end
171
+ res = [ op_name, *annotate_values(db, opts) ]
172
+
173
+ if upstreams_valid?(db, opts) && scope.valid? && include_counts?(db, opts)
174
+ scope.with_ctes(self, db)
175
+ .from_self
176
+ .select_group(:criterion_domain)
177
+ .select_append { Sequel.function(:count, 1).as(:rows) }
178
+ .select_append { count(:person_id).distinct.as(:n) }
179
+ .each do |h|
180
+ counts[h.delete(:criterion_domain).to_sym] = h
181
+ end
182
+ elsif !errors.empty?
183
+ annotation[:errors] = errors
184
+ scope.add_errors(scope_key, errors)
185
+ end
186
+
187
+ domains(db).each do |domain|
188
+ cur_counts = counts[domain] ||= { rows: 0, n: 0 }
189
+ scope.add_counts(scope_key, domain, cur_counts)
190
+ end
191
+
192
+ if defined?(@warnings) && !warnings.empty?
193
+ annotation[:warnings] = warnings
194
+ scope.add_warnings(scope_key, warnings.dup)
195
+ end
196
+
197
+ if res.last.is_a?(Hash)
198
+ res.last.merge!(metadata)
199
+ else
200
+ res << metadata
201
+ end
202
+
203
+ @annotate = res
204
+ end
205
+
206
+ def code_list(db)
207
+ upstreams.flat_map { |upstream_op| upstream_op.code_list(db) }
41
208
  end
42
209
 
43
- def scope=(scope)
44
- @scope = scope
45
- scope.add_operator(self)
210
+ def inspect
211
+ "<##{self.class} upstreams=[#{upstreams.map(&:inspect).join(', ')}] arguments=[#{arguments.map(&:inspect).join(', ')}]>"
46
212
  end
47
213
 
48
214
  def evaluate(db)
49
- select_it(query(db))
215
+ q = select_it(query(db))
216
+ name = cte_name(op_name)
217
+ opts = {}
218
+ opts[:materialized] = true if rdbms.supports_materialized?
219
+ db[name].with(name, q, opts)
220
+ end
221
+
222
+ def pretty_print(pp)
223
+ pp.object_group self do
224
+ unless pp_upstreams.empty?
225
+ pp.breakable
226
+ pp.text "@upstreams="
227
+ pp.pp pp_upstreams
228
+ pp.comma_breakable unless arguments.empty?
229
+ end
230
+ unless arguments.empty?
231
+ pp.breakable if pp_upstreams.empty?
232
+ pp.text "@arguments="
233
+ pp.pp arguments
234
+ end
235
+ end
236
+ end
237
+
238
+ def pp_upstreams
239
+ upstreams
240
+ end
241
+
242
+ def all_upstreams
243
+ upstreams
50
244
  end
51
245
 
52
246
  def sql(db)
53
247
  evaluate(db).sql
54
248
  end
55
249
 
56
- def select_it(query, specific_type = nil)
57
- specific_type = type if specific_type.nil? && respond_to?(:type)
58
- q = query.select(*columns(query, specific_type))
59
- if scope && scope.person_ids && upstreams.empty?
60
- q = q.where(person_id: scope.person_ids).from_self
250
+ def select_it(query, opts = {})
251
+ specific_table = opts[:specific_table]
252
+ specific_table ||= dm.determine_table(:source_table)
253
+ specific_table ||= dm.determine_table(:table)
254
+ specific_table ||= dm.determine_table(:domain)
255
+
256
+ dom = begin
257
+ opts[:domain] || domain
258
+ rescue StandardError
259
+ nil
61
260
  end
62
- q
261
+
262
+ opts = {
263
+ table: specific_table,
264
+ criterion_domain: dom,
265
+ query_columns: override_columns,
266
+ uuid: opts[:uuid] || options[:uuid]
267
+ }
268
+
269
+ cf = column_family || (specific_table ? ConceptQL::Scope::DEFAULT_COLUMN_FAMILY : nil)
270
+ opts[:replace] ||= {}
271
+ opts[:replace][:label] = label if label
272
+ opts[:replace][:column_family] = cf if cf
273
+
274
+ query = query.comment(comment) if comments?
275
+
276
+ dm.selectify(query, opts)
63
277
  end
64
278
 
65
- def types
66
- @types ||= determine_types
279
+ def comments?
280
+ ENV["CONCEPTQL_ENABLE_COMMENTS"] == "true"
281
+ end
282
+
283
+ def comment
284
+ PP.pp(self, "".dup, 10)
285
+ end
286
+
287
+ def override_columns
288
+ nil
289
+ end
290
+
291
+ def domains(db)
292
+ @domains ||= determine_domains(db)
293
+ end
294
+
295
+ def tables
296
+ @tables ||= determine_tables
67
297
  end
68
298
 
69
299
  def stream
70
300
  @stream ||= upstreams.first
71
301
  end
72
302
 
73
- def columns(query, local_type = nil)
74
- criterion_type = :criterion_type
75
- if local_type
76
- criterion_type = Sequel.cast_string(local_type.to_s).as(:criterion_type)
77
- end
78
- columns = [:person_id,
79
- type_id(local_type),
80
- criterion_type]
81
- columns += date_columns(query, local_type)
82
- columns += value_columns(query, local_type)
303
+ def columns(table = nil)
304
+ dm.columns(table: table)
83
305
  end
84
306
 
85
307
  def label
86
- options[:label]
308
+ @label ||= begin
309
+ options.delete(:label) if options[:label] && options[:label].to_s.strip.empty?
310
+ options[:label].respond_to?(:strip) ? options[:label].strip : options[:label]
311
+ end
87
312
  end
88
313
 
89
- private
90
- attr :scope
314
+ def column_family; end
315
+
316
+ def valid?(db, opts = {})
317
+ validate(db, opts)
318
+ errors.empty?
319
+ end
320
+
321
+ def upstreams_valid?(db, opts = {})
322
+ valid?(db, opts) && upstreams.all? { |u| u.upstreams_valid?(db, opts) }
323
+ end
324
+
325
+ def scope
326
+ nodifier.scope
327
+ end
328
+
329
+ def data_model
330
+ nodifier.data_model
331
+ end
332
+
333
+ def database_type
334
+ nodifier.database_type
335
+ end
336
+
337
+ def cast_column(column, value = nil)
338
+ type = Scope::COLUMN_TYPES.fetch(column)
339
+ case type
340
+ when String, :String
341
+ Sequel.cast_string(value).as(column)
342
+ when Date, :Date
343
+ Sequel.cast(value, type).as(column)
344
+ when Float, :Bigint, :Float
345
+ Sequel.cast_numeric(value, type).as(column)
346
+ else
347
+ raise "Unexpected type: '#{type.inspect}' for column: '#{column}'"
348
+ end
349
+ end
350
+
351
+ def omopv4_plus?
352
+ data_model == :omopv4_plus
353
+ end
354
+
355
+ def gdm?
356
+ %i[gdm gdm_wide].include?(data_model)
357
+ end
91
358
 
92
- def criterion_id
93
- :criterion_id
359
+ def wide?
360
+ %i[gdm_wide].include?(data_model)
94
361
  end
95
362
 
96
- def type_id(type = nil)
97
- return :criterion_id if type.nil?
98
- type = :person if type == :death
99
- Sequel.expr(make_type_id(type)).as(:criterion_id)
363
+ def dm
364
+ @dm ||= DataModel.for(self, nodifier)
100
365
  end
101
366
 
102
- def make_type_id(type)
103
- (type.to_s + '_id').to_sym
367
+ def rdbms
368
+ dm.rdbms
369
+ end
370
+
371
+ def query_cols
372
+ raise NotImplementedError, self
373
+ end
374
+
375
+ def events_per_patient
376
+ if upstreams.first
377
+ upstreams.first.events_per_patient
378
+ else
379
+ :multiple
380
+ end
381
+ end
382
+
383
+ def vocabularies
384
+ if respond_to?(:vocab_entry)
385
+ [ vocab_entry.omopv5_vocabulary_id ]
386
+ elsif upstreams.any?
387
+ upstreams.flat_map(&:vocabularies).uniq
388
+ else
389
+ []
390
+ end
391
+ end
392
+
393
+ def multiple_vocabularies
394
+ if upstreams.any?
395
+ upstreams.first.multiple_vocabularies
396
+ else
397
+ false
398
+ end
399
+ end
400
+
401
+ def accept(visitor, opts = {})
402
+ visitor.visit(self)
403
+ upstreams.each { |u| u.accept(visitor, opts) }
404
+ end
405
+
406
+ private
407
+
408
+ def annotate_values(db, opts)
409
+ (upstreams.map { |op| op.annotate(db, opts) } + arguments).push(options)
104
410
  end
105
411
 
106
412
  def make_table_name(table)
107
- "#{table}___tab".to_sym
108
- end
109
-
110
- def value_columns(query, type)
111
- [
112
- numeric_value(query),
113
- string_value(query),
114
- concept_id_value(query),
115
- units_source_value(query),
116
- source_value(query, type)
117
- ]
118
- end
119
-
120
- def numeric_value(query)
121
- return :value_as_number if query.columns.include?(:value_as_number)
122
- Sequel.cast_numeric(nil, Float).as(:value_as_number)
123
- end
124
-
125
- def string_value(query)
126
- return :value_as_string if query.columns.include?(:value_as_string)
127
- Sequel.cast_string(nil).as(:value_as_string)
128
- end
129
-
130
- def concept_id_value(query)
131
- return :value_as_concept_id if query.columns.include?(:value_as_concept_id)
132
- Sequel.cast_numeric(nil).as(:value_as_concept_id)
133
- end
134
-
135
- def units_source_value(query)
136
- return :units_source_value if query.columns.include?(:units_source_value)
137
- Sequel.cast_string(nil).as(:units_source_value)
138
- end
139
-
140
- def source_value(query, type)
141
- return :source_value if query.columns.include?(:source_value)
142
- Sequel.cast_string(source_value_column(query, type)).as(:source_value)
143
- end
144
-
145
- def date_columns(query, type = nil)
146
- return [:start_date, :end_date] if (query.columns.include?(:start_date) && query.columns.include?(:end_date))
147
- return [:start_date, :end_date] unless type
148
- sd = start_date_column(query, type)
149
- sd = Sequel.expr(sd).cast(:date).as(:start_date) unless sd == :start_date
150
- ed = end_date_column(query, type)
151
- ed = Sequel.function(:coalesce, Sequel.expr(ed).cast(:date), Sequel.expr(start_date_column(query, type))).as(:end_date) unless ed == :end_date
152
- [sd, ed]
153
- end
154
-
155
- def start_date_column(query, type)
156
- {
157
- condition_occurrence: :condition_start_date,
158
- death: :death_date,
159
- drug_exposure: :drug_exposure_start_date,
160
- drug_cost: nil,
161
- payer_plan_period: :payer_plan_period_start_date,
162
- person: person_date_of_birth(query),
163
- procedure_occurrence: :procedure_date,
164
- procedure_cost: nil,
165
- observation: :observation_date,
166
- observation_period: :observation_period_start_date,
167
- visit_occurrence: :visit_start_date
168
- }[type]
169
- end
170
-
171
- def end_date_column(query, type)
172
- {
173
- condition_occurrence: :condition_end_date,
174
- death: :death_date,
175
- drug_exposure: :drug_exposure_end_date,
176
- drug_cost: nil,
177
- payer_plan_period: :payer_plan_period_end_date,
178
- person: person_date_of_birth(query),
179
- procedure_occurrence: :procedure_date,
180
- procedure_cost: nil,
181
- observation: :observation_date,
182
- observation_period: :observation_period_end_date,
183
- visit_occurrence: :visit_end_date
184
- }[type]
185
- end
186
-
187
- def source_value_column(query, type)
188
- {
189
- condition_occurrence: :condition_source_value,
190
- death: :cause_of_death_source_value,
191
- drug_exposure: :drug_source_value,
192
- drug_cost: nil,
193
- payer_plan_period: :payer_plan_period_source_value,
194
- person: :person_source_value,
195
- procedure_occurrence: :procedure_source_value,
196
- procedure_cost: nil,
197
- observation: :observation_source_value,
198
- observation_period: nil,
199
- visit_occurrence: :place_of_service_source_value
200
- }[type]
201
- end
202
-
203
- def person_date_of_birth(query)
204
- assemble_date(query, :year_of_birth, :month_of_birth, :day_of_birth)
205
- end
206
-
207
- def assemble_date(query, *symbols)
208
- strings = symbols.map do |symbol|
209
- Sequel.cast_string(Sequel.function(:coalesce, Sequel.cast_string(symbol), Sequel.cast_string('01')))
210
- end
211
- strings = strings.zip(['-'] * (symbols.length - 1)).flatten.compact
212
- concatted_strings = Sequel.join(strings)
213
- case query.db.database_type
214
- when :oracle
215
- Sequel.function(:to_date, concatted_strings, 'YYYY-MM-DD')
216
- when :mssql
217
- Sequel.lit('CONVERT(DATETIME, ?)', concatted_strings)
413
+ Sequel.as(table, :tab)
414
+ end
415
+
416
+ def query_columns(query)
417
+ dm.query_columns(query)
418
+ end
419
+
420
+ def additional_columns(query, table)
421
+ special_columns = {
422
+ code_provenance_type: proc { code_provenance_type(query, table) },
423
+ file_provenance_type: proc { file_provenance_type(query, table) },
424
+ provider_id: proc { provider_id(query, table) },
425
+ visit_source_concept_id: proc { dm.place_of_service_concept_id(query, table) }
426
+ }
427
+
428
+ additional_cols = special_columns.each_with_object([]) do |(column, proc_obj), columns|
429
+ columns << proc_obj.call if dynamic_columns.include?(column)
430
+ end
431
+
432
+ standard_columns = dynamic_columns - Scope::DEFAULT_COLUMNS.keys
433
+ standard_columns -= special_columns.keys
434
+
435
+ standard_columns.each do |column|
436
+ additional_cols << if query_columns(query).include?(column)
437
+ column
438
+ else
439
+ cast_column(column)
440
+ end
441
+ end
442
+
443
+ additional_cols
444
+ end
445
+
446
+ def source_value(query, table)
447
+ return :source_value if query_columns(query).include?(:source_value)
448
+
449
+ cast_column(:source_value, dm.source_value_column(query, table))
450
+ end
451
+
452
+ def source_vocabulary_id(query, table)
453
+ return :source_vocabulary_id if query_columns(query).include?(:source_vocabulary_id)
454
+
455
+ cast_column(:source_vocabulary_id, dm.source_vocabulary_id(query, table))
456
+ end
457
+
458
+ def code_provenance_type(query, table)
459
+ return :code_provenance_type if query_columns(query).include?(:code_provenance_type)
460
+
461
+ cast_column(:code_provenance_type, dm.provenance_type_column(query, table))
462
+ end
463
+
464
+ def file_provenance_type(query, table)
465
+ return :file_provenance_type if query_columns(query).include?(:file_provenance_type)
466
+
467
+ cast_column(:file_provenance_type, dm.provenance_type_column(query, table))
468
+ end
469
+
470
+ def provider_id(query, table)
471
+ return :provider_id if query_columns(query).include?(:provider_id)
472
+
473
+ cast_column(:provider_id, dm.provider_id_column(query, table))
474
+ end
475
+
476
+ def determine_tables
477
+ if upstreams.empty?
478
+ if respond_to?(:table)
479
+ [ table ]
480
+ else
481
+ [ :invalid ]
482
+ end
218
483
  else
219
- Sequel.cast(concatted_strings, Date)
484
+ tables = upstreams.compact.map(&:tables).flatten.uniq
485
+ tables.empty? ? [ :invalid ] : tables
220
486
  end
221
487
  end
222
488
 
223
- def determine_types
489
+ def determine_domains(db)
224
490
  if upstreams.empty?
225
- if respond_to?(:type)
226
- [type]
491
+ if respond_to?(:domain)
492
+ [ domain ]
227
493
  else
228
- raise "Operator doesn't seem to specify any type"
494
+ [ :invalid ]
229
495
  end
230
496
  else
231
- upstreams.map(&:types).flatten.uniq
497
+ doms = upstreams.compact.flat_map { |up| up.domains(db) }.uniq
498
+ doms.empty? ? [ :invalid ] : doms
499
+ end
500
+ end
501
+
502
+ # Validation Related
503
+
504
+ def upstream_operator_names
505
+ @upstreams.map(&:op_name)
506
+ end
507
+
508
+ def validate(db, opts = {})
509
+ return if @_validated
510
+
511
+ @errors = [] unless defined?(@errors)
512
+ @warnings = [] unless defined?(@warnings)
513
+
514
+ add_error("invalid label") if label && !label.is_a?(String)
515
+ self.class.validations.each do |args|
516
+ send(*args)
517
+ end
518
+
519
+ additional_validation(db, opts)
520
+
521
+ @_validated = true
522
+ end
523
+
524
+ def validate_no_upstreams
525
+ add_error("has upstreams", upstream_operator_names) unless @upstreams.empty?
526
+ end
527
+
528
+ def validate_one_upstream
529
+ validate_at_least_one_upstream
530
+ validate_at_most_one_upstream
531
+ end
532
+
533
+ def validate_at_most_one_upstream
534
+ add_error("has multiple upstreams", upstream_operator_names) if @upstreams.length > 1
535
+ end
536
+
537
+ def validate_at_least_one_upstream
538
+ add_error("has no upstream") if @upstreams.empty?
539
+ end
540
+
541
+ def validate_no_arguments
542
+ add_error("has arguments", @arguments) unless @arguments.empty?
543
+ end
544
+
545
+ def validate_one_argument
546
+ validate_at_least_one_argument
547
+ validate_at_most_one_argument
548
+ end
549
+
550
+ def validate_at_most_one_argument
551
+ add_error("has multiple arguments", @arguments) if @arguments.length > 1
552
+ end
553
+
554
+ def validate_at_least_one_argument
555
+ add_error("has no arguments") if @arguments.empty?
556
+ end
557
+
558
+ def validate_option(format, *opts)
559
+ opts.each do |opt|
560
+ next unless options.key?(opt)
561
+
562
+ add_error("wrong option format", opt.to_s, options[opt]) unless format === options[opt]
563
+ end
564
+ end
565
+
566
+ def validate_required_options(*opts)
567
+ opts.each do |opt|
568
+ add_error("required option not present", opt.to_s) unless options.key?(opt)
569
+ end
570
+ end
571
+
572
+ # A blank value counts as not set, as in Occurrence#at_least_option
573
+ def validate_unsupported_options(*opts)
574
+ opts.each do |opt|
575
+ next if options[opt].to_s.strip.empty?
576
+
577
+ add_error("unsupported option", opt.to_s, options[opt])
578
+ end
579
+ end
580
+
581
+ def bad_arguments
582
+ return [] unless code_regexp
583
+ # Vocabulary#select_all? is private, so include private methods.
584
+ return [] if respond_to?(:select_all?, true) && select_all?
585
+
586
+ @bad_arguments ||= arguments.reject do |arg|
587
+ code_regexp === arg
588
+ end
589
+ end
590
+
591
+ # Only Vocabulary operators carry a code format (Vocabulary#code_regexp)
592
+ def code_regexp
593
+ nil
594
+ end
595
+
596
+ def validate_codes_match
597
+ return if bad_arguments.empty?
598
+
599
+ add_warning("improperly formatted code", *bad_arguments)
600
+ end
601
+
602
+ def additional_validation(_db, _opts = {})
603
+ # Do nothing by default
604
+ end
605
+
606
+ def add_warnings?(db, opts = {})
607
+ @errors.empty? && !no_db?(db, opts)
608
+ end
609
+
610
+ def add_error(*args)
611
+ errors << args
612
+ end
613
+
614
+ def add_warning(*args)
615
+ warnings << args
616
+ end
617
+
618
+ def include_counts?(db, opts)
619
+ !(no_db?(db, opts) || opts[:skip_counts])
620
+ end
621
+
622
+ def skip_db?(opts)
623
+ opts[:skip_db]
624
+ end
625
+
626
+ def no_db?(db, opts = {})
627
+ no_db = db.nil? || db.adapter_scheme == :mock || skip_db?(opts)
628
+ no_db ||= table_is_missing?(db)
629
+ no_db
630
+ end
631
+
632
+ def table_is_missing?(_db)
633
+ false
634
+ end
635
+
636
+ def preferred_name
637
+ self.class.pref_name
638
+ end
639
+
640
+ def semi_or_inner_join(ds, table, *exprs)
641
+ ds = Sequel[ds] if ds.is_a?(Symbol)
642
+ table = Sequel[table] if table.is_a?(Symbol)
643
+ expr = exprs.inject(&:&)
644
+ if use_inner_join?
645
+ ds.join(table.as(:r), expr)
646
+ .select(*query_cols.map { |c| Sequel[:l][c] })
647
+ else
648
+ rdbms.semi_join(ds, table, *exprs)
649
+ end
650
+ end
651
+
652
+ def matching_columns
653
+ if scope.opts.dig(:window_opts, :window_table)
654
+ %i[person_id window_id]
655
+ else
656
+ [ :person_id ]
657
+ end
658
+ end
659
+
660
+ def use_inner_join?
661
+ options[:inner_join]
662
+ end
663
+
664
+ # The names in an option that lists columns (join_columns, only_columns
665
+ # and so on), as symbols. A JSON statement gives them as strings, and
666
+ # Sequel renders a String as a string literal, not as a column. Call it
667
+ # only for options that name columns: other options are free-form.
668
+ #
669
+ # The value is an Array or a comma-separated String, such as
670
+ # "uuid, start_date". A blank String is an unset option, so it gives
671
+ # nil.
672
+ def column_names_option(name)
673
+ value = options[name]
674
+ if value.is_a?(String)
675
+ value = value.split(",").map(&:strip).reject(&:empty?)
676
+ return nil if value.empty?
232
677
  end
678
+ value&.map(&:to_sym)
233
679
  end
234
680
  end
235
681
  end