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,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "operator"
4
+
5
+ module ConceptQL
6
+ module Operators
7
+ class Vocabulary < Operator
8
+ include ConceptQL::Behaviors::Windowable
9
+ include ConceptQL::Behaviors::CodeLister
10
+
11
+ category "Select by Clinical Codes"
12
+ basic_type :selection
13
+ desc "Selects records based on the given vocabulary."
14
+ validate_no_upstreams
15
+ validate_at_least_one_argument
16
+ validate_codes_match
17
+
18
+ def source_table
19
+ domain_map(op_name)
20
+ end
21
+
22
+ def table
23
+ domain_map(op_name)
24
+ end
25
+
26
+ def domain
27
+ domain_map(op_name)
28
+ end
29
+
30
+ def domains(_db)
31
+ vocab_entry.predominant_domains.map(&:to_sym)
32
+ end
33
+
34
+ def additional_validation(db, opts = {})
35
+ return unless add_warnings?(db, opts) && !select_all?
36
+
37
+ args = arguments.dup
38
+ args -= bad_arguments
39
+
40
+ missing_args = args - dm.known_codes(db, vocabulary_id, args)
41
+
42
+ return if missing_args.empty?
43
+
44
+ add_warning("unknown code(s)", *missing_args)
45
+ end
46
+
47
+ def describe_codes(db, codes)
48
+ return [ [ "*", "ALL CODES" ] ] if select_all?
49
+
50
+ found_codes = dm.concepts_to_codes(db, vocabulary_id, codes)
51
+
52
+ found_codes + (codes - found_codes.map(&:first)).zip([])
53
+ end
54
+
55
+ def filter_clause(db)
56
+ filter_clause = Sequel[where_clause(db)]
57
+ if (ex_clause = exclusion_clause(db)).present?
58
+ filter_clause &= ~Sequel[ex_clause]
59
+ end
60
+ filter_clause
61
+ end
62
+
63
+ def criterion_table
64
+ dm.table_by_domain(domain)
65
+ end
66
+
67
+ # The condition that selects this vocabulary's rows. where_clause
68
+ # includes it, so Union can name every merged vocabulary in one IN list.
69
+ def vocabulary_condition
70
+ { clinical_code_vocabulary_id: vocabulary_id }
71
+ end
72
+
73
+ private
74
+
75
+ def query(db)
76
+ ds = db[criterion_table]
77
+
78
+ ds = ds.where(filter_clause(db))
79
+
80
+ unless (more_columns = additional_columns(db)).empty?
81
+ ds = ds.select_append(*more_columns)
82
+ end
83
+
84
+ ds
85
+ end
86
+
87
+ def exclusion_clause(_db)
88
+ {}
89
+ end
90
+
91
+ def additional_columns(_db)
92
+ []
93
+ end
94
+
95
+ def where_clause(db)
96
+ # We're probably going to partition the observations table by clinical_code_vocabulary_id,
97
+ # for Spark, so this will create queries that leverage that partitioning
98
+ # and it shouldn't hurt for PostgreSQL performance
99
+ wheres = vocabulary_condition
100
+
101
+ unless select_all?
102
+ concept_ids = dm.concept_ids(db, vocabulary_id, arguments)
103
+
104
+ wheres = wheres.merge({ clinical_code_concept_id: concept_ids })
105
+ end
106
+
107
+ wheres
108
+ end
109
+
110
+ def query_cols
111
+ dm.table_columns(source_table)
112
+ end
113
+
114
+ def select_all?
115
+ arguments.include?("*")
116
+ end
117
+
118
+ def preferred_name
119
+ vocab_entry.short_name || vocab_entry.omopv5_id
120
+ end
121
+
122
+ # Defined so that bad_arguments can check for bad codes
123
+ def code_regexp
124
+ unless defined?(@code_regexp)
125
+ @code_regexp = nil
126
+
127
+ if (reg_str = vocab_entry.format_regexp)
128
+ @code_regexp = Regexp.union(Regexp.new(reg_str, Regexp::IGNORECASE), /\A\*\Z/)
129
+ end
130
+ end
131
+ @code_regexp
132
+ end
133
+
134
+ def vocabulary_id
135
+ @vocabulary_id ||= translated_vocabulary_id
136
+ end
137
+
138
+ def translated_vocabulary_id
139
+ vocab_entry.omopv5_vocabulary_id || op_name
140
+ end
141
+
142
+ def domain_map(_v_id)
143
+ (vocab_entry.domain || :condition_occurrence).to_sym
144
+ end
145
+
146
+ def table_is_missing?(db)
147
+ dm.table_is_missing?(db)
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Pathname is core only from Ruby 4.0; on 3.x a clean `gem install` (no Bundler
4
+ # preloading it) fails in ConceptQL.root without this. RuboCop targets 4.0.
5
+ require "pathname"
6
+
7
+ module ConceptQL
8
+ class << self
9
+ def root
10
+ Pathname.new(__dir__).parent.parent.expand_path
11
+ end
12
+
13
+ def schemas_dir
14
+ root / "schemas"
15
+ end
16
+
17
+ def config_dir
18
+ root / "config"
19
+ end
20
+
21
+ def vocabularies_file_path
22
+ ConceptQL.config_dir / "vocabularies.csv"
23
+ end
24
+
25
+ def multiple_vocabularies_file_path
26
+ ConceptQL.config_dir / "multiple_vocabularies.csv"
27
+ end
28
+
29
+ def custom_vocabularies_file_path
30
+ Pathname.new(ENV["CONCEPTQL_CUSTOM_VOCABULARIES"] || ConceptQL.config_dir / "vocabularies.custom.csv")
31
+ end
32
+
33
+ def custom_multiple_vocabularies_file_path
34
+ Pathname.new(ENV["CONCEPTQL_CUSTOM_MULTIPLE_VOCABULARIES"] || ConceptQL.config_dir / "multiple_vocabularies.custom.csv")
35
+ end
36
+
37
+ def race_file
38
+ ConceptQL.config_dir / "race.yml"
39
+ end
40
+ end
41
+ end
@@ -1,50 +1,215 @@
1
- require 'psych'
2
- require 'forwardable'
3
- require_relative 'behaviors/preppable'
4
- require_relative 'tree'
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "forwardable"
5
+ require_relative "scope"
6
+ require_relative "nodifier"
7
+ require_relative "sql_formatters"
5
8
 
6
9
  module ConceptQL
7
10
  class Query
11
+ class QueryError < StandardError
12
+ def message
13
+ return super unless cause
14
+
15
+ og_message = if cause.respond_to?(:full_message)
16
+ cause.full_message
17
+ else
18
+ ([ cause.message ] + cause.backtrace).join("\n")
19
+ end
20
+ [ super, "OG ERROR:", og_message ].join("\n")
21
+ end
22
+ end
23
+
24
+ # Raised by #code_list when the statement has validation errors: the
25
+ # operator tree of an invalid statement can have missing operands, and a
26
+ # partial code list would look complete. #errors is the errors hash of
27
+ # #scope_annotate (operator name => list of [message, *details]).
28
+ class InvalidStatementError < QueryError
29
+ attr_reader :statement, :errors
30
+
31
+ def initialize(statement, errors)
32
+ @statement = statement
33
+ @errors = errors
34
+ super([ "Invalid ConceptQL statement, so it has no code list. Validation errors:",
35
+ *errors.flat_map { |op, op_errors| op_errors.map { |e| " #{op}: #{describe(e)}" } },
36
+ "Statement: #{statement.inspect}" ].join("\n"))
37
+ end
38
+
39
+ private
40
+
41
+ def describe(error)
42
+ message, *details = error
43
+ details.empty? ? message.to_s : "#{message} (#{details.flatten.map(&:to_s).join(', ')})"
44
+ end
45
+ end
46
+
8
47
  extend Forwardable
9
- def_delegators :prepped_query, :all, :count, :execute, :order
48
+ def_delegators :query, :all, :count, :execute, :order
49
+ def_delegators :cdb, :db
50
+
51
+ attr_reader :statement
52
+
53
+ def initialize(cdb, statement, opts = {})
54
+ @cdb = cdb
55
+ @statement, opts = extract_statement(statement, opts)
56
+ @opts = opts
57
+ opts[:algorithm_fetcher] ||= proc do |alg|
58
+ statement, description = db[:concepts].where(concept_id: alg).get(%i[statement label])
59
+ statement = JSON.parse(statement) if statement.is_a?(String)
60
+ [ statement, description ]
61
+ end
62
+ @nodifier = opts[:nodifier] || Nodifier.new(cdb, { database_type: cdb.database_type }.merge(opts))
63
+ end
64
+
65
+ def analyze
66
+ query(explain: true, analyze: true).analyze
67
+ end
10
68
 
11
- attr :statement
12
- def initialize(db, statement, tree = Tree.new)
13
- @db = db
14
- @db.extend_datasets(ConceptQL::Behaviors::Preppable)
15
- @statement = statement
16
- @tree = tree
69
+ def query(opts = {})
70
+ (@queries ||= {})[opts] ||= nodifier.scope.with_ctes(operator, db, opts)
17
71
  end
18
72
 
19
- def query
20
- build_query(db)
73
+ def query_cols(opts = {})
74
+ cols = operator.dynamic_columns
75
+ if opts[:cast]
76
+ cols = query_cols.to_h do |column|
77
+ [ column, operator.cast_column(column) ]
78
+ end
79
+ end
80
+ cols
81
+ end
82
+
83
+ def column_names
84
+ query_cols.is_a?(Hash) ? query_cols.keys : query_cols
85
+ end
86
+
87
+ def sql(*args)
88
+ sql_statements(*args).values.join(";\n")
89
+ end
90
+
91
+ def sql_statements(*args)
92
+ stmts = query.sql_statements
93
+
94
+ if args.include?(:create_tables)
95
+ sql = stmts.delete(:query)
96
+ drop_stmts = []
97
+ if args.include?(:drop_tables)
98
+ drop_stmts = stmts.map do |name, _|
99
+ [ [ "drop", name ].join("_"), db.send(:drop_table_sql, name, if_exists: true) ]
100
+ end
101
+ end
102
+ create_stmts = stmts.map do |name, sql|
103
+ [ name, db.send(:create_table_as_sql, name, sql, {}) ]
104
+ end
105
+ stmts = drop_stmts + create_stmts
106
+ stmts.push([ :query, sql ])
107
+ end
108
+
109
+ if args.include?(:formatted)
110
+ stmts = stmts.map do |name, sql|
111
+ [ name, format(sql) ]
112
+ end
113
+ end
114
+ Hash[stmts]
115
+ rescue StandardError
116
+ raise QueryError, "Failed to generate SQL for #{statement.inspect}"
21
117
  end
22
118
 
23
- def sql
24
- (tree.scope.sql(db) << operator.sql(db)).join(";\n\n") + ';'
119
+ def annotate(opts = {})
120
+ operator.annotate(db, opts)
25
121
  end
26
122
 
27
- def types
28
- tree.root(self).types
123
+ def accept(visitor, _opts = {})
124
+ operator.accept(visitor)
125
+ end
126
+
127
+ def extract_metadata
128
+ visitor = ConceptQL::Visitors::MetadataExtractor.new
129
+ operator.accept(visitor)
130
+ visitor.results
131
+ end
132
+
133
+ def scope_annotate(opts = {})
134
+ annotate(opts)
135
+ nodifier.scope.annotation
136
+ end
137
+
138
+ def domains
139
+ operator.domains(db)
140
+ end
141
+
142
+ def events_per_patient
143
+ operator.events_per_patient
144
+ end
145
+
146
+ def multiple_vocabularies
147
+ operator.multiple_vocabularies
29
148
  end
30
149
 
31
150
  def operator
32
- @operator ||= tree.root(self)
151
+ @operator ||= if statement.is_a?(Array)
152
+ if statement.first.is_a?(Array)
153
+ Operators::Invalid.new(nodifier, "invalid", errors: [ [ "incomplete statement" ] ])
154
+ else
155
+ nodifier.create(*statement)
156
+ end
157
+ else
158
+ Operators::Invalid.new(nodifier, "invalid",
159
+ errors: [ [ "invalid root operator", statement.inspect ] ])
160
+ end
33
161
  end
34
162
 
35
- private
36
- attr :yaml, :tree, :db
163
+ def rdbms
164
+ operator.rdbms
165
+ end
166
+
167
+ # Raises InvalidStatementError if the statement has validation errors.
168
+ # Warnings do not raise.
169
+ def code_list(_ignored_db = nil)
170
+ errors = validation_errors
171
+ raise InvalidStatementError.new(statement, errors) unless errors.empty?
172
+
173
+ operator.code_list(db).uniq
174
+ end
175
+
176
+ # The errors that #scope_annotate reports, the same check that callers use
177
+ # to decide whether a statement is valid. A fresh query runs it, because
178
+ # operators memoize #annotate and #validate, and this query's later
179
+ # #annotate calls must not get those results.
180
+ def validation_errors
181
+ Query.new(cdb, statement, @opts).scope_annotate(skip_counts: true)[:errors]
182
+ end
37
183
 
38
- def build_query(db)
39
- operator.evaluate(db).tap { |q| q.prep_proc = prep_proc }
184
+ def drop_temp_tables(opts = {})
185
+ query.drop_temp_tables(opts)
40
186
  end
41
187
 
42
- def prep_proc
43
- @prep_proc = Proc.new { puts 'PREPPING'; tree.scope.prep(db) }
188
+ private
189
+
190
+ attr_reader :cdb, :nodifier
191
+
192
+ def extract_statement(stmt, opts)
193
+ if !stmt.is_a?(Array)
194
+ raise "Improper ConceptQL statement: Expected an Array, got a #{stmt.class}"
195
+ elsif stmt.first.to_s == "window"
196
+ raise "window operator needs a hash as the last item of the array." unless stmt.last.is_a?(Hash)
197
+ unless stmt.length == 3
198
+ raise "window operator needs a ConceptQL statement followed by a hash as the last item of the array."
199
+ end
200
+
201
+ opts[:scope_opts] = (opts[:scope_opts] || {})
202
+ opts[:scope_opts].merge!(window_opts: stmt.last.merge(cdb: cdb))
203
+ extract_statement(stmt[1], opts)
204
+ elsif stmt.length == 1 && stmt.first.is_a?(Array)
205
+ extract_statement(stmt.first, opts)
206
+ else
207
+ [ stmt, opts ]
208
+ end
44
209
  end
45
210
 
46
- def prepped_query
47
- query
211
+ def format(sql)
212
+ SqlFormatters.format(sql, rdbms)
48
213
  end
49
214
  end
50
215
  end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../query_modifier"
4
+
5
+ module ConceptQL
6
+ module QueryModifiers
7
+ module Gdm
8
+ class AdmissionDateQueryModifier < QueryModifier
9
+ def self.provided_columns
10
+ %i[
11
+ admission_date
12
+ discharge_date
13
+ ]
14
+ end
15
+
16
+ def self.has_required_columns?(cols)
17
+ needed = [ :collection_id ]
18
+ found = needed & cols
19
+ needed == found
20
+ end
21
+
22
+ def modified_query
23
+ return query unless dm.table_cols(source_table).include?(:collection_id)
24
+
25
+ query.from_self(alias: :cc)
26
+ .left_join(:collections, { id: :collection_id }, table_alias: :co)
27
+ .left_join(:admission_details, { id: :admission_detail_id }, table_alias: :ad)
28
+ .select_all(:cc)
29
+ .select_append(Sequel.function(:coalesce, Sequel[:ad][:admission_date],
30
+ Sequel[:cc][:start_date]).as(:admission_date))
31
+ .select_append(Sequel.function(:coalesce, Sequel[:ad][:discharge_date], Sequel[:cc][:end_date],
32
+ Sequel[:cc][:start_date]).as(:discharge_date))
33
+ .from_self
34
+ end
35
+
36
+ private
37
+
38
+ def domain
39
+ op.domain
40
+ rescue StandardError
41
+ nil
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../query_modifier"
4
+
5
+ module ConceptQL
6
+ module QueryModifiers
7
+ module Gdm
8
+ class DrugQueryModifier < QueryModifier
9
+ def self.provided_columns
10
+ %i[
11
+ drug_name
12
+ drug_amount
13
+ drug_amount_units
14
+ drug_quantity
15
+ drug_strength_source_value
16
+ drug_days_supply
17
+ ]
18
+ end
19
+
20
+ def self.has_required_columns?(cols)
21
+ needed = [ :drug_exposure_detail_id ]
22
+ found = needed & cols
23
+ needed == found
24
+ end
25
+
26
+ def modified_query
27
+ return query unless dm.table_cols(source_table).include?(:drug_exposure_detail_id)
28
+
29
+ # TODO: Determine what actual columns to include for drug exposures under
30
+ query.from_self(alias: :cc)
31
+ .left_join(Sequel[:drug_exposure_details].as(:de), Sequel[:cc][:drug_exposure_detail_id] => Sequel[:de][:id])
32
+ .left_join(dm.name_concepts_table(query.db).as(:dose_con), Sequel[:de][:dose_unit_concept_id] => Sequel[:dose_con][:id])
33
+ .left_join(dm.name_concepts_table(query.db).as(:ing_con), Sequel[:cc][:clinical_code_concept_id] => Sequel[:ing_con][:id])
34
+ .select_all(:cc)
35
+ .select_append(Sequel[:de][:dose_value].as(:drug_amount))
36
+ .select_append(Sequel[:dose_con][:concept_text].as(:drug_amount_units))
37
+ .select_append(Sequel[:ing_con][:concept_text].as(:drug_name))
38
+ .select_append(Sequel[:de][:days_supply].as(:drug_days_supply))
39
+ .select_append(Sequel[:de][:strength_source_value].as(:drug_strength_source_value))
40
+ .select_append(Sequel[:cc][:quantity].as(:drug_quantity))
41
+ .from_self
42
+ end
43
+
44
+ private
45
+
46
+ def domain
47
+ op.domain
48
+ rescue StandardError
49
+ nil
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../query_modifier"
4
+
5
+ module ConceptQL
6
+ module QueryModifiers
7
+ module Gdm
8
+ class LabQueryModifier < QueryModifier
9
+ def self.provided_columns
10
+ %i[
11
+ value_as_number
12
+ value_as_string
13
+ value_as_concept_id
14
+ value_as_concept
15
+ unit_concept_id
16
+ unit_concept
17
+ range_low
18
+ range_high
19
+ ]
20
+ end
21
+
22
+ def self.has_required_columns?(cols)
23
+ needed = [ :measurement_detail_id ]
24
+ found = needed & cols
25
+ needed == found
26
+ end
27
+
28
+ def modified_query
29
+ return query unless dm.table_cols(source_table).include?(:measurement_detail_id)
30
+
31
+ query.from_self(alias: :cc)
32
+ .left_join(Sequel[:measurement_details].as(:md), Sequel[:cc][:measurement_detail_id] => Sequel[:md][:id])
33
+ .left_join(dm.name_concepts_table(query.db).as(:unit_con), Sequel[:md][:unit_concept_id] => Sequel[:unit_con][:id])
34
+ .left_join(dm.name_concepts_table(query.db).as(:concept_con), Sequel[:md][:result_as_concept_id] => Sequel[:concept_con][:id])
35
+ .select_all(:cc)
36
+ .select_append(Sequel[:md][:result_as_number].as(:value_as_number))
37
+ .select_append(Sequel[:md][:result_as_string].as(:value_as_string))
38
+ .select_append(Sequel[:md][:result_as_concept_id].as(:value_as_concept_id))
39
+ .select_append(Sequel[:concept_con][:concept_code].as(:value_as_concept))
40
+ .select_append(Sequel[:md][:unit_concept_id].as(:unit_concept_id))
41
+ .select_append(Sequel[:unit_con][:concept_code].as(:unit_concept))
42
+ .select_append(Sequel[:md][:normal_range_low].as(:range_low))
43
+ .select_append(Sequel[:md][:normal_range_high].as(:range_high))
44
+ .from_self
45
+ end
46
+
47
+ private
48
+
49
+ def domain
50
+ op.domain
51
+ rescue StandardError
52
+ nil
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../query_modifier"
4
+
5
+ module ConceptQL
6
+ module QueryModifiers
7
+ module Gdm
8
+ class PoSQueryModifier < QueryModifier
9
+ attr_reader :join_id, :table, :source_column, :column
10
+
11
+ def self.provided_columns
12
+ [ :visit_source_concept_id ]
13
+ end
14
+
15
+ def self.has_required_columns?(cols)
16
+ needed = [ :context_id ]
17
+ found = needed & cols
18
+ needed == found
19
+ end
20
+
21
+ def initialize(*args)
22
+ super
23
+ @column = :visit_source_concept_id
24
+ @join_id = :context_id
25
+ @table = :contexts
26
+ @source_column = :pos_concept_id
27
+ end
28
+
29
+ def modified_query
30
+ return query unless dm.table_cols(source_table).include?(join_id)
31
+
32
+ left_alias = "tab1".to_sym
33
+ right_alias = "tab2".to_sym
34
+
35
+ extra_table = query.db.from(table).select(Sequel.as(source_column, column), :id)
36
+ ds = query.from_self(alias: left_alias)
37
+ ds.left_join(extra_table.as(right_alias),
38
+ Sequel.qualify(left_alias, join_id) => Sequel.qualify(right_alias, :id))
39
+ .select_all(left_alias)
40
+ .select_append(Sequel.qualify(right_alias, column).as(column))
41
+ .from_self
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../query_modifier"
4
+
5
+ module ConceptQL
6
+ module QueryModifiers
7
+ module Gdm
8
+ class ProvenanceQueryModifier < QueryModifier
9
+ RELATED_COLUMNS = %w[
10
+ provenance_concept_id
11
+ source_type_concept_id
12
+ ].sort.map(&:to_sym)
13
+
14
+ attr_reader :db
15
+
16
+ def self.provided_columns
17
+ %i[
18
+ file_provenance_type
19
+ code_provenance_type
20
+ ]
21
+ end
22
+
23
+ def self.has_required_columns?(cols)
24
+ !(RELATED_COLUMNS & cols).empty?
25
+ end
26
+
27
+ def initialize(*args)
28
+ super
29
+ @db = query.db
30
+ end
31
+
32
+ def modified_query
33
+ return query unless self.class.has_required_columns?(dm.table_cols(source_table))
34
+
35
+ query.from_self(alias: :cc)
36
+ .left_join(Sequel[:contexts].as(:c), Sequel[:cc][:context_id] => Sequel[:c][:id])
37
+ .select_all(:cc)
38
+ .select_append(Sequel[:c][:source_type_concept_id].as(:file_provenance_type),
39
+ Sequel[:cc][:provenance_concept_id].as(:code_provenance_type))
40
+ .from_self
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end