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,214 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "logger"
5
+ require_relative "lexicon"
6
+
7
+ module ConceptQL
8
+ class Database
9
+ attr_reader :db, :opts
10
+
11
+ @lexicon_mutex = Mutex.new
12
+
13
+ EXTENSIONS = %i[
14
+ date_arithmetic
15
+ error_sql
16
+ make_readyable
17
+ null_dataset
18
+ pg_ctas_explain
19
+ pg_vacuum_table
20
+ smart_select_remove
21
+ sql_comments
22
+ usable
23
+ ].freeze
24
+
25
+ def initialize(db, opts = {})
26
+ @db = db
27
+ # Symbolize all keys and values
28
+ @opts = ConceptQL::Utils.rekey(opts, rekey_values: true)
29
+
30
+ @opts[:data_model] ||= (ENV["CONCEPTQL_DATA_MODEL"] || ConceptQL::DEFAULT_DATA_MODEL).to_sym
31
+
32
+ self.class.db_extensions(db, @opts[:data_model], @opts[:use_cold_col])
33
+ db_type = db ? db.database_type.to_sym : :postgres
34
+
35
+ @lexicon = opts[:lexicon]
36
+ if @lexicon && @opts[:cache_lookups]
37
+ raise ArgumentError, "cache_lookups needs the Database to build its own Lexicon; do not pass lexicon:"
38
+ end
39
+
40
+ @opts[:database_type] ||= (ENV["CONCEPTQL_DATABASE_TYPE"] || db_type).to_sym
41
+ @opts[:scope_opts] = {
42
+ force_temp_tables: opts.fetch(:force_temp_tables, ENV["CONCEPTQL_FORCE_TEMP_TABLES"] == "true"),
43
+ scratch_database: opts.fetch(:scratch_database, ENV["DOCKER_SCRATCH_DATABASE"])
44
+ }.merge(opts[:scope_opts] || {})
45
+ @opts[:scope_opts][:lexicon] = lexicon
46
+
47
+ @shared_utilization_tables = {}
48
+ @shared_utilization_mutex = Mutex.new
49
+ @source_tables = {} if @opts[:cache_lookups]
50
+ end
51
+
52
+ def query(statement, opts = {})
53
+ opts[:scope_opts] = (@opts[:scope_opts] || {}).merge(opts.delete(:scope_opts) || {})
54
+ if @opts[:share_utilization] && opts[:scope_opts][:table_prefix].to_s.empty?
55
+ raise ArgumentError, "share_utilization needs a table_prefix in scope_opts, to name its tables"
56
+ end
57
+
58
+ Query.new(self, ConceptQL::Utils.rekey(statement), @opts.merge(opts))
59
+ end
60
+
61
+ def data_model
62
+ @data_model ||= DataModel.get(opts[:data_model], nodifier: nodifier)
63
+ end
64
+
65
+ def nodifier
66
+ @nodifier ||= Nodifier.new(self)
67
+ end
68
+
69
+ def base_data_model
70
+ data_model.base
71
+ end
72
+
73
+ class << self
74
+ def db_extensions(db, data_model, use_cold_col)
75
+ return unless db
76
+
77
+ EXTENSIONS.each do |extension|
78
+ db.extension extension
79
+ end
80
+
81
+ use_cold_col = db.is_a?(Sequel::Mock::Database) if use_cold_col.nil?
82
+ return unless use_cold_col
83
+
84
+ db.extension(:cold_col)
85
+ db.load_schema(ConceptQL.schemas_dir / "#{data_model}.yml")
86
+ db.load_schema(ConceptQL.schemas_dir / "ohdsi_vocabs.yml")
87
+ end
88
+
89
+ def lexicon_db
90
+ @lexicon_mutex.synchronize do
91
+ @lexicon_db = make_lexicon_db unless defined?(@lexicon_db)
92
+ end
93
+ @lexicon_db
94
+ end
95
+
96
+ # The LEXICON_URL database, or nil when it is unset. Lexicon tries the
97
+ # data db first and, when neither has the vocabulary tables, falls back
98
+ # to LexiconNoDB, so no database (and no sqlite3 gem) is needed here.
99
+ def make_lexicon_db
100
+ return unless ENV["LEXICON_URL"]
101
+
102
+ db_opts = {}
103
+ if ENV["CONCEPTQL_LOG_LEXICON"]
104
+ log_path = Pathname.new("log") / "conceptql_lexicon.log"
105
+ log_path.dirname.mkpath
106
+ db_opts[:logger] = Logger.new(log_path)
107
+ end
108
+ lexicon_db = Sequel.connect(ENV["LEXICON_URL"], db_opts)
109
+ lexicon_db.extension(:date_arithmetic)
110
+ lexicon_db
111
+ end
112
+ end
113
+
114
+ # Whether a data model's source table exists in db. With cache_lookups on,
115
+ # a table that exists is remembered per db and table for the life of this
116
+ # Database, as the Lexicon keeps its lookups. A missing table is checked
117
+ # again on every call: Sequel's table_exists? also answers false on a
118
+ # database error, and one transient error must not turn off the database
119
+ # checks for the whole export. Only the data model's fixed source tables
120
+ # come here, never a table that conceptql creates during a run.
121
+ def source_table_exists?(db, table)
122
+ return db.table_exists?(table) unless @source_tables
123
+
124
+ key = [ db, table ]
125
+ @source_tables.fetch(key) { db.table_exists?(table) && (@source_tables[key] = true) }
126
+ end
127
+
128
+ def lexicon
129
+ @lexicon ||= Lexicon.new(self.class.lexicon_db, db, cache: @opts[:cache_lookups])
130
+ end
131
+
132
+ # This Database's operators for a data model, as a frozen name => class
133
+ # Hash: a vocabulary operator for every vocabulary in config/vocabularies.csv
134
+ # (plus the custom file) and in this Database's lexicon, with the built-in
135
+ # operators merged over them. A built-in wins a name clash (gender, race and
136
+ # ethnicity are also vocabularies). Built lazily, once per data model.
137
+ #
138
+ # The data model is an argument because a Nodifier can be given one that
139
+ # differs from this Database's.
140
+ def operators(data_model = opts[:data_model])
141
+ data_model = data_model.to_sym
142
+ @operators ||= {}
143
+ @operators[data_model] ||= begin
144
+ vocabulary_operators = Vocabularies::DynamicVocabularies.new(lexicon).operators(data_model)
145
+ vocabulary_operators.merge(Operators.static_operators.fetch(data_model)).freeze
146
+ end
147
+ end
148
+
149
+ # Builds the table that the name_concepts_table option names. It holds
150
+ # the concept rows whose ids appear in the data columns that the
151
+ # display-name joins read, so those joins hash a small table instead of
152
+ # the whole concept table. The caller drops it.
153
+ def create_name_concepts_table
154
+ table = opts[:name_concepts_table]
155
+ raise ArgumentError, "create_name_concepts_table needs the name_concepts_table option" unless table
156
+
157
+ ids = data_model.name_concept_columns.flat_map do |source, columns|
158
+ next [] unless db.table_exists?(source)
159
+
160
+ (columns & db[source].columns).map { |column| db[source].exclude(column => nil).select(Sequel.as(column, :id)) }
161
+ end
162
+ raise ArgumentError, "no data columns to read for data model #{opts[:data_model]}" if ids.empty?
163
+
164
+ db.create_table(table, as: data_model.concepts_table(db).where(id: ids.reduce { |all, one| all.union(one) }))
165
+ end
166
+
167
+ # The share_utilization option: the name of the table that holds a
168
+ # Utilizable operator's list (its select_it(query(db)), passed as key).
169
+ # The key's SQL contains the window table and adjustments, so two
170
+ # different windows get two tables. The name is a Symbol, because
171
+ # sequel-duckdb's table_exists? reads a Sequel::SQL::Identifier wrong.
172
+ def shared_utilization_table(key, scope)
173
+ name = :"#{scope.opts[:table_prefix]}_util_#{Digest::MD5.hexdigest(key.sql)[0, 16]}"
174
+ scope.scratch_database.to_s.empty? ? name : Sequel.qualify(scope.scratch_database.to_sym, name)
175
+ end
176
+
177
+ # The sql_statements entry that creates a shared_utilization_table, or nil
178
+ # when the table exists. The caller runs the entries in order. Nothing is
179
+ # claimed: every call lists a table until it exists.
180
+ def shared_utilization_statement(name, key)
181
+ return if db.table_exists?(name)
182
+
183
+ @shared_utilization_mutex.synchronize { @shared_utilization_tables[key.sql] = name }
184
+ [ name, key.sql ]
185
+ end
186
+
187
+ # Creates a shared_utilization_table that does not exist yet, when a
188
+ # query first reads rows through conceptql. Another thread that reads the
189
+ # same table waits here and then reads the table the first thread created.
190
+ def create_shared_utilization_table(name, key, rdbms, scope)
191
+ @shared_utilization_mutex.synchronize do
192
+ next if db.table_exists?(name)
193
+
194
+ db.create_table(name, rdbms.create_options(scope, key).merge(as: key))
195
+ rdbms.post_create(db, name)
196
+ @shared_utilization_tables[key.sql] = name
197
+ end
198
+ end
199
+
200
+ # The shared tables that conceptql listed in sql_statements or created.
201
+ # conceptql never drops them: the caller does.
202
+ def shared_tables
203
+ @shared_utilization_mutex.synchronize { @shared_utilization_tables.values }
204
+ end
205
+
206
+ def database_type
207
+ @opts[:database_type]
208
+ end
209
+
210
+ def type_for(column_name)
211
+ Scope::COLUMN_TYPES.fetch(column_name)
212
+ end
213
+ end
214
+ end
@@ -1,43 +1,91 @@
1
- require_relative '../conceptql'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../conceptql"
2
4
 
3
5
  module ConceptQL
4
6
  # Used to translate a string of terse date adjustments into a set of adjustments that are compatible with most RDBMSs
5
7
  class DateAdjuster
6
- attr :str
7
- def initialize(str)
8
- @str = str
8
+ VALID_INPUT = /\A#{Regexp.union([ /START/i, /END/i, /\d{4}-\d{2}-\d{2}/, /[ers]*([-+]?[\ddwmy]+)+/i,
9
+ /\s*/ ])}\z/.freeze
10
+
11
+ attr_reader :op, :str, :manipulator
12
+
13
+ def initialize(op, str, opts = {})
14
+ str = str.strftime("%Y-%m-%d") if str.respond_to?(:strftime)
15
+ raise "Invalid adjustment string: #{str.pretty_inspect}" unless str.nil? || str =~ VALID_INPUT
16
+
17
+ @op = op
18
+ @str = str || ""
19
+ @manipulator = opts[:manipulator] || Sequel
20
+ end
21
+
22
+ def adjust(column, reverse = false)
23
+ return Sequel.expr(:end_date) if str.downcase == "end"
24
+ return Sequel.expr(:start_date) if str.downcase == "start"
25
+ return op.rdbms.cast_date(Date.parse(str).strftime("%Y-%m-%d")) if str =~ /^\d{4}-\d{2}-\d{2}$/
26
+
27
+ origin_column = column
28
+
29
+ chrs = str.chars
30
+ while (chr = chrs.shift) =~ /[res]/i
31
+ if chr.downcase == "r"
32
+ reverse = true
33
+ else
34
+ origin_column = chr.downcase == "e" ? :end_date : :start_date
35
+ origin_column = Sequel.qualify(column.table.to_sym, origin_column) if column.respond_to?(:table)
36
+ end
37
+ end
38
+
39
+ adjusted_date = adjustments(reverse).inject(Sequel.expr(origin_column)) do |sql, (units, quantity)|
40
+ # Turns out weeks aren't supported in Sequel, so we'll just multiply
41
+ # the number of weeks by 7 and adjust the date by that number of days
42
+ if units == :weeks
43
+ units = :days
44
+ quantity *= 7
45
+ end
46
+
47
+ if quantity.positive?
48
+ manipulator.date_add(sql, units => quantity)
49
+ else
50
+ manipulator.date_sub(sql, units => quantity.abs)
51
+ end
52
+ end
53
+ op.rdbms.cast_date(adjusted_date)
9
54
  end
10
55
 
11
56
  # Returns an array of strings that represent date modifiers
12
- def adjustments
13
- @adjustments ||= parse(str)
57
+ def adjustments(reverse = false)
58
+ @adjustments ||= parse(str, reverse)
14
59
  end
15
60
 
16
61
  private
62
+
17
63
  def lookup
18
64
  {
19
- 'y' => :years,
20
- 'm' => :months,
21
- 'd' => :days
65
+ "y" => :years,
66
+ "m" => :months,
67
+ "w" => :weeks,
68
+ "d" => :days
22
69
  }
23
70
  end
24
71
 
25
- def parse(str)
26
- ConceptQL.logger.debug(str)
72
+ def parse(str, reverse)
27
73
  return [] if str.nil? || str.empty?
28
- return [[lookup['d'], str.to_i]] if str.match(/^[-+]?\d+$/)
29
- str.downcase.scan(/([-+]?\d*[dmy])/).map do |adjustment|
74
+ return [ [ lookup["d"], str.to_i ] ] if str.match(/^[-+]?\d+$/)
75
+
76
+ str.downcase.scan(/([-+]?\d*[dwmy])/).map do |adjustment|
30
77
  adjustment = adjustment.first
78
+
31
79
  quantity = 1
32
80
  if adjustment.match(/\d/)
33
81
  quantity = adjustment.to_i
34
- else
35
- if adjustment.chars.first == '-'
36
- quantity = -1
37
- end
82
+ elsif adjustment.chars.first == "-"
83
+ quantity = -1
38
84
  end
85
+ quantity *= -1 if reverse
86
+
39
87
  unit = lookup[adjustment.chars.last]
40
- [unit, quantity]
88
+ [ unit, quantity ]
41
89
  end
42
90
  end
43
91
  end
@@ -0,0 +1,243 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "active_support/core_ext/hash/indifferent_access"
5
+
6
+ module ConceptQL
7
+ # Converts ConceptQL statements into trees a diagram renderer can draw.
8
+ #
9
+ # Two shapes are produced:
10
+ #
11
+ # * RenderTree -- the `conceptql-diagram/v1` format: one node per operator,
12
+ # carrying the operator's metadata (base type, human name) and what
13
+ # conceptql's own annotation worked out for it (output types, errors,
14
+ # warnings and, optionally, per-domain counts).
15
+ # * JigsawTree -- the Jigsaw diagram editor's tree JSON (name, values,
16
+ # parameters, children), as produced by the editor's ConceptqlAdapter.
17
+ module Diagram
18
+ FORMAT = "conceptql-diagram/v1"
19
+
20
+ class << self
21
+ # The full `conceptql-diagram/v1` document for one or more statements.
22
+ #
23
+ # Without `counts: true` no claims database is needed: pass a cdb built
24
+ # on a nil db (the default).
25
+ def render(statements, cdb: default_cdb, counts: false)
26
+ {
27
+ format: FORMAT,
28
+ statements: RenderTree.new(cdb, counts: counts).build_all(statements)
29
+ }
30
+ end
31
+
32
+ def default_cdb(data_model = nil)
33
+ opts = {}
34
+ opts[:data_model] = data_model.to_sym if data_model
35
+ ConceptQL::Database.new(nil, opts)
36
+ end
37
+
38
+ # A statement file may hold a single statement or a list of them.
39
+ # Mirrors JDE's ConceptqlAdapter#nested.
40
+ def statement_list(statements)
41
+ statements = JSON.parse(statements) if statements.is_a?(String)
42
+ return [] if statements == []
43
+ return statements if statements.first.is_a?(Array)
44
+
45
+ [ statements ]
46
+ end
47
+ end
48
+
49
+ class RenderTree
50
+ # Operator options that are structure or bookkeeping, not parameters
51
+ # a diagram should show.
52
+ STRUCTURAL_OPTIONS = %i[annotation name left right id].freeze
53
+
54
+ attr_reader :cdb
55
+
56
+ def initialize(cdb, counts: false)
57
+ @cdb = cdb
58
+ @counts = counts
59
+ end
60
+
61
+ def counts?
62
+ @counts
63
+ end
64
+
65
+ def build_all(statements)
66
+ Diagram.statement_list(statements).map { |statement| build(statement) }
67
+ end
68
+
69
+ def build(statement)
70
+ node(cdb.query(statement).annotate(skip_counts: !counts?))
71
+ end
72
+
73
+ private
74
+
75
+ def node(annotated)
76
+ name, *rest = annotated
77
+ hashes, rest = rest.partition { |v| v.is_a?(Hash) }
78
+ opts = hashes.reduce({}, :merge)
79
+ upstreams, values = rest.partition { |v| v.is_a?(Array) }
80
+ annotation = opts[:annotation] || {}
81
+
82
+ children = upstreams + opts.values_at(:left, :right).compact
83
+ node = {
84
+ name: name.to_s,
85
+ base: metadata_for(name)[:basic_type]&.to_s,
86
+ humanName: opts[:name] || metadata_for(name)[:preferred_name] || name.to_s
87
+ }
88
+ node[:vocabularyId] = metadata_for(name)[:preferred_name] if vocabulary?(name)
89
+ node.merge!(
90
+ outputTypes: output_types(annotation),
91
+ parameters: opts.except(*STRUCTURAL_OPTIONS),
92
+ values: values,
93
+ children: children.map { |child| node(child) }
94
+ )
95
+ node[:counts] = counts(annotation) if counts?
96
+ node[:errors] = annotation[:errors] if annotation[:errors].present?
97
+ node[:warnings] = annotation[:warnings] if annotation[:warnings].present?
98
+ node
99
+ end
100
+
101
+ # conceptql fills annotation[:counts] from Operator#domains, so its keys
102
+ # are the node's output types. Vocabulary metadata nests them
103
+ # ([["condition_occurrence"]]) where cast operators do not ([:person]),
104
+ # so flatten defensively. Sorted so that a counted run (whose keys arrive
105
+ # in database order) matches an uncounted one.
106
+ def output_types(annotation)
107
+ (annotation[:counts] || {}).keys.flatten.map(&:to_s).uniq.sort
108
+ end
109
+
110
+ def counts(annotation)
111
+ (annotation[:counts] || {})
112
+ .sort_by { |domain, _| domain.to_s }
113
+ .to_h { |domain, c| [ domain.to_s, { rows: Integer(c[:rows] || 0), n: Integer(c[:n] || 0) } ] }
114
+ end
115
+
116
+ def metadata_for(name)
117
+ operators_metadata[canonical_name(name)] || {}
118
+ end
119
+
120
+ def operators_metadata
121
+ @operators_metadata ||= ConceptQL.metadata(cdb)[:operators]
122
+ end
123
+
124
+ # A single-vocabulary operator (icd9, loinc, ...). Its metadata
125
+ # preferred_name is the vocabulary's ID (Entry#preferred_name:
126
+ # omopv5_id || id, e.g. "ICD9CM", "LOINC"), which a renderer can show
127
+ # when the humanName (the vocabulary's short name) is too long.
128
+ # Multi-vocabulary operators ("CPT or HCPCS") span several IDs, so they
129
+ # are not included.
130
+ def vocabulary?(name)
131
+ klass = operator_classes[canonical_name(name)]
132
+ !klass.nil? && klass <= ConceptQL::Operators::Vocabulary
133
+ end
134
+
135
+ def operator_classes
136
+ @operator_classes ||= cdb.operators(cdb.opts[:data_model])
137
+ end
138
+
139
+ def canonical_name(name)
140
+ name = name.to_s
141
+ operators_metadata.key?(name) ? name : aliases[name]
142
+ end
143
+
144
+ def aliases
145
+ @aliases ||= operators_metadata.each_with_object({}) do |(name, md), h|
146
+ Array(md[:aliases]).each { |a| h[a.to_s] = name }
147
+ end
148
+ end
149
+ end
150
+
151
+ # Port of the Jigsaw diagram editor's ConceptqlAdapter#to_jigsaw_json.
152
+ # The only change is where binary (filter) operators are looked up: the
153
+ # editor asked its Rails-wide cdb, this takes one (db-less by default).
154
+ class JigsawTree
155
+ BINARY_PARAMETERS = %w[left right].freeze
156
+ IGNORED_PARAMETERS = %w[id].freeze
157
+
158
+ class Node
159
+ def initialize(operator, binary)
160
+ @operator = operator
161
+ @binary = binary
162
+ end
163
+
164
+ def name
165
+ operator[0]
166
+ end
167
+
168
+ def values
169
+ operator[1..].select { |v| v.is_a?(String) || v.is_a?(Integer) }
170
+ end
171
+
172
+ def parameters
173
+ return unless operator[-1].is_a?(Hash)
174
+
175
+ operator[-1]
176
+ .reject { |k, _v| IGNORED_PARAMETERS.include?(k.to_s) }
177
+ .with_indifferent_access
178
+ end
179
+
180
+ def children
181
+ operator[1..right_bound].grep(Array)
182
+ end
183
+
184
+ def binary?
185
+ @binary.include?(name)
186
+ end
187
+
188
+ private
189
+
190
+ attr_reader :operator
191
+
192
+ def right_bound
193
+ parameters.present? ? -2 : -1
194
+ end
195
+ end
196
+
197
+ def initialize(statements, cdb: Diagram.default_cdb)
198
+ @statements = Diagram.statement_list(statements)
199
+ @cdb = cdb
200
+ end
201
+
202
+ def to_jigsaw_json
203
+ { name: "root", children: statements.map { |s| build_node(s) } }
204
+ end
205
+
206
+ private
207
+
208
+ attr_reader :statements, :cdb
209
+
210
+ def binary
211
+ @binary ||= ConceptQL.metadata(cdb)[:operators].select { |_k, v| v[:basic_type] == :filter }.keys
212
+ end
213
+
214
+ def build_node(operator)
215
+ node = Node.new(operator, binary)
216
+ {
217
+ name: node.name,
218
+ values: node.values,
219
+ parameters: build_parameters(node),
220
+ children: build_children(node)
221
+ }.reject { |_k, v| v.blank? }
222
+ end
223
+
224
+ def build_parameters(node)
225
+ return unless node.parameters
226
+
227
+ node.parameters.reject { |p| BINARY_PARAMETERS.include?(p.to_s) }
228
+ end
229
+
230
+ def build_children(node)
231
+ kids = node.children.map { |child| build_node(child) }
232
+ kids += build_binary_children(node) if node.binary?
233
+ kids
234
+ end
235
+
236
+ def build_binary_children(node)
237
+ return [] unless node.parameters
238
+
239
+ BINARY_PARAMETERS.filter_map { |p| build_node(node.parameters[p]) if node.parameters[p] }
240
+ end
241
+ end
242
+ end
243
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lexicon_strategy"
4
+
5
+ module ConceptQL
6
+ class LexiconGDM < LexiconStrategy
7
+ class << self
8
+ def db_has_all_vocabulary_tables?(db)
9
+ %i[ancestors concepts mappings vocabularies].all? { |t| db.table_exists?(t) }
10
+ end
11
+ end
12
+
13
+ def vocabularies_query
14
+ db[:vocabularies]
15
+ .select(Sequel[:id].as(:id),
16
+ Sequel[:id].as(:omopv5_vocabulary_id),
17
+ Sequel[:vocabulary_name].as(:vocabulary_short_name),
18
+ Sequel[:vocabulary_name].as(:vocabulary_full_name),
19
+ Sequel[:domain],
20
+ Sequel.expr(1).as(:from_lexicon))
21
+ end
22
+
23
+ def vocabularies
24
+ vocabularies_query.all
25
+ end
26
+
27
+ def is_a_relationships(_data_db)
28
+ db[:mappings].where { Sequel.function(:lower, :relationship_id) =~ "is_a" }
29
+ end
30
+
31
+ def ancestors_table(_data_db)
32
+ db[:ancestors]
33
+ end
34
+
35
+ def concepts_table(_data_db, _some_schema = nil)
36
+ db[:concepts]
37
+ end
38
+
39
+ def table_is_missing?(_data_db)
40
+ !db.table_exists?(:concepts)
41
+ end
42
+
43
+ def strategy
44
+ :gdm
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lexicon_strategy"
4
+
5
+ module ConceptQL
6
+ # The strategy used when neither the dataset db nor the lexicon db has
7
+ # vocabulary tables (for instance a db-less ConceptQL::Database, as the
8
+ # diagram renderer uses). Its db is a Sequel mock, so every lookup returns
9
+ # no rows, but SQL that joins or selects from the vocabulary tables can
10
+ # still be BUILT.
11
+ #
12
+ # It must answer the same table methods as the other strategies, because
13
+ # LexiconStrategy's lookups (concepts, descendants_of,
14
+ # related_concept_ids, ...) are written against them. The tables are named
15
+ # in the GDM layout (concepts / ancestors / mappings), matching
16
+ # #vocabularies_query, which already reads GDM's `vocabularies` table, and
17
+ # the columns that LexiconStrategy's queries use (id, concept_code,
18
+ # vocabulary_id, concept_text; ancestor_id / descendant_id;
19
+ # concept_1_id / concept_2_id).
20
+ class LexiconNoDB < LexiconStrategy
21
+ def vocabularies_query
22
+ db[:vocabularies]
23
+ end
24
+
25
+ def vocabularies
26
+ []
27
+ end
28
+
29
+ def is_a_relationships(_data_db)
30
+ db[:mappings].where { Sequel.function(:lower, :relationship_id) =~ "is_a" }
31
+ end
32
+
33
+ def ancestors_table(_data_db)
34
+ db[:ancestors]
35
+ end
36
+
37
+ def concepts_table(_data_db, _some_schema = nil)
38
+ db[:concepts]
39
+ end
40
+
41
+ def table_is_missing?(_db)
42
+ true
43
+ end
44
+
45
+ def strategy
46
+ :no_db
47
+ end
48
+ end
49
+ end