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,36 +1,128 @@
1
- require_relative 'binary_operator_operator'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "binary_operator_operator"
4
+ require_relative "../date_adjuster"
2
5
 
3
6
  module ConceptQL
4
7
  module Operators
5
8
  # Base class for all temporal operators
6
9
  #
7
10
  # Subclasses must implement the where_clause method which should probably return
8
- # a proc that can be executed as a Sequel "virtual row" e.g.
9
- # Proc.new { l.end_date < r.start_date }
11
+ # a Sequel expression to use for filtering.
10
12
  class TemporalOperator < BinaryOperatorOperator
11
13
  reset_categories
12
- category %w(Temporal Relative)
14
+ category "Filter by Comparing"
15
+ default_query_columns
16
+
17
+ option :within, type: :string,
18
+ instructions: 'Enter a numeric value and specify "d", "m", or "y" for "days", "months", or "years". Negative numbers change dates prior to the existing date. Example: -30d = 30 days before the existing date.'
19
+
20
+ # The temporal operators read join_columns through
21
+ # BinaryOperatorOperator#join_columns_option, but do not declare it with
22
+ # `option`: only t_shank gives it, and a declared option would appear in
23
+ # ConceptQL.metadata, so the diagram editor would offer it (Ryan,
24
+ # 2026-10-26).
25
+
26
+ validate_option DateAdjuster::VALID_INPUT, :within, :at_least
27
+
28
+ class << self
29
+ def allows_at_least_option
30
+ option :at_least, type: :string,
31
+ instructions: 'Enter a numeric value and specify "d", "m", or "y" for "days", "months", or "years". Negative numbers change dates prior to the existing date. Example: -30d = 30 days before the existing date.'
32
+ end
33
+ end
34
+
35
+ def self.within_skip(type)
36
+ define_method(:"within_check_#{type}?") { false }
37
+ end
13
38
 
14
39
  def query(db)
15
- db.from(db.from(left_stream(db))
16
- .join(right_stream(db), l__person_id: :r__person_id)
17
- .where(where_clause)
18
- .select_all(:l))
40
+ right_stream = apply_selectors(right_stream_query(db), function: rhs_function).from_self(alias: :r)
41
+ ds = semi_or_inner_join(left_stream(db), right_stream, *join_columns, where_clause)
42
+
43
+ ds = apply_selectors(ds, qualifier: :r)
44
+
45
+ ds.from_self
46
+ end
47
+
48
+ def r_start_date
49
+ Sequel[:r][:start_date]
50
+ end
51
+
52
+ def r_end_date
53
+ Sequel[:r][:end_date]
54
+ end
55
+
56
+ def l_start_date
57
+ Sequel[:l][:start_date]
58
+ end
59
+
60
+ def l_end_date
61
+ Sequel[:l][:end_date]
62
+ end
63
+
64
+ def within_option
65
+ return unless (v = options[:within])
66
+ return if v.strip.empty?
67
+
68
+ v
69
+ end
70
+
71
+ def at_least_option
72
+ return unless (v = options[:at_least])
73
+ return if v.strip.empty?
74
+
75
+ v
76
+ end
77
+
78
+ def within_source_table
79
+ Sequel[:r]
80
+ end
81
+
82
+ def within_start
83
+ within_start = within_source_table[:start_date]
84
+ within_start = adjust_date(within_option, within_start, true) if within_option
85
+ within_start
86
+ end
87
+
88
+ def within_end
89
+ within_end = within_source_table[:end_date]
90
+ within_end = adjust_date(within_option, within_end) if within_option
91
+ within_end
19
92
  end
20
93
 
21
- def inclusive?
22
- options[:inclusive]
94
+ def adjust_date(adjustment, column, reverse = false)
95
+ adjuster = DateAdjuster.new(self, adjustment)
96
+ adjuster.adjust(column, reverse)
23
97
  end
24
98
 
25
- def left_stream(db)
26
- Sequel.expr(left.evaluate(db)).as(:l)
99
+ def apply_selectors(ds, opts = {})
100
+ return ds unless include_rhs_columns
101
+
102
+ ds = ds.select_remove(*include_rhs_columns).select_append(*rhs_columns(opts)) if include_rhs_columns
103
+ ds
104
+ end
105
+
106
+ def rhs_columns(opts)
107
+ cols = include_rhs_columns
108
+ return cols if opts.empty?
109
+
110
+ cols = cols.map { |c| Sequel.function(opts[:function], c).as(c) } if opts[:function]
111
+ cols = cols.map { |c| Sequel[opts[:qualifier]][c].as(c) } if opts[:qualifier]
112
+ cols
113
+ end
114
+
115
+ def include_rhs_columns
116
+ options[:include_rhs_columns]&.map(&:to_sym)
117
+ end
118
+
119
+ def rhs_function
120
+ nil
27
121
  end
28
122
 
29
- def right_stream(db)
30
- Sequel.expr(right.evaluate(db)).as(:r)
123
+ def use_inner_join?
124
+ super || options[:include_rhs_columns]
31
125
  end
32
126
  end
33
127
  end
34
128
  end
35
-
36
-
@@ -1,5 +1,7 @@
1
- require_relative 'operator'
2
- require_relative '../date_adjuster'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "pass_thru"
4
+ require_relative "../date_adjuster"
3
5
 
4
6
  module ConceptQL
5
7
  module Operators
@@ -19,22 +21,27 @@ module ConceptQL
19
21
  # Both start and end arguments must be provided, but if you do not wish to adjust a date just
20
22
  # pass '', '0', or nil as that argument. E.g.:
21
23
  # start: 'd', end: '' # Only adjust start_date by positive 1 day and leave end_date uneffected
22
- class TimeWindow < Operator
23
- desc 'Adjusts the start_date and end_date columns to create a new window of time for each result.'
24
- option :start, type: :string
25
- option :end, type: :string
24
+ class TimeWindow < PassThru
25
+ register __FILE__
26
+
27
+ desc "Adjusts start_date and end_date to create a new window of time for each record."
28
+ option :start, type: :string,
29
+ instructions: 'Enter a numeric value and specify "d", "m", or "y" for "days", "months", or "years". Negative numbers change dates prior to the existing date.'
30
+ option :end, type: :string,
31
+ instructions: 'Enter a numeric value and specify "d", "m", or "y" for "days", "months", or "years". Negative numbers change dates prior to the existing date.'
26
32
  allows_one_upstream
27
- category %(Temporal Manipulation)
33
+ validate_one_upstream
34
+ validate_no_arguments
35
+ validate_option DateAdjuster::VALID_INPUT, :start, :end
36
+ category "Modify Data"
37
+ basic_type :temporal
28
38
 
29
39
  def query(db)
30
- db.extension :date_arithmetic
31
- db.from(stream.evaluate(db))
40
+ dm.selectify(db.from(stream.evaluate(db)).from_self,
41
+ replace: { start_date: adjusted_start_date, end_date: adjusted_end_date })
32
42
  end
33
43
 
34
44
  private
35
- def date_columns(query, type = nil)
36
- [adjusted_start_date, adjusted_end_date]
37
- end
38
45
 
39
46
  def adjusted_start_date
40
47
  adjusted_date(:start, :start_date)
@@ -44,28 +51,9 @@ module ConceptQL
44
51
  adjusted_date(:end, :end_date)
45
52
  end
46
53
 
47
- def manipulator
48
- @manipulator ||= options.fetch(:date_manipulator, Sequel)
49
- end
50
-
51
- # NOTE: This produces PostgreSQL-specific date adjustment. I'm not yet certain how to generalize this
52
- # or make different versions based on RDBMS
53
54
  def adjusted_date(option_arg, column)
54
- arg = options[option_arg]
55
- arg ||= ''
56
- return ['end_date', column].join('___').to_sym if arg.downcase == 'end'
57
- return ['start_date', column].join('___').to_sym if arg.downcase == 'start'
58
- return Sequel.cast(Date.parse(arg).strftime('%Y-%m-%d'), Date).as(column) if arg =~ /^\d{4}-\d{2}-\d{2}$/
59
- adjusted_date = DateAdjuster.new(arg).adjustments.inject(Sequel.expr(column)) do |sql, (units, quantity)|
60
- if quantity > 0
61
- manipulator.date_add(sql, units => quantity)
62
- else
63
- manipulator.date_sub(sql, units => quantity.abs)
64
- end
65
- end
66
- Sequel.cast(adjusted_date, Date).as(column)
55
+ DateAdjuster.new(self, options[option_arg], manipulator: options[:manipulator]).adjust(column)
67
56
  end
68
57
  end
69
58
  end
70
59
  end
71
-
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "temporal_operator"
4
+
5
+ module ConceptQL
6
+ module Operators
7
+ # Trims the end_date of the LHS set of records by the RHS's earliest
8
+ # start_date (per person)
9
+ # If a the RHS contains a start_date that comes before the LHS's start_date
10
+ # that LHS record is completely discarded.
11
+ #
12
+ # If there is no RHS record for an LHS record, the LHS record is passed
13
+ # thru unaffected.
14
+ #
15
+ # If the RHS record's start_date is later than the LHS end_date, the LHS
16
+ # record is passed thru unaffected.
17
+ class TrimDate < TemporalOperator
18
+ desc <<~EOF
19
+ Trims the end_date of the left hand records (LHR) by the earliest
20
+ start_date (per person) in the right hand records (RHR)
21
+ If the RHR contains a start_date that comes before the start_date in the LHR
22
+ that record in the LHR is completely discarded.
23
+
24
+ If there is no record in the RHR for a record in the LHR, the record in the LHR is passed
25
+ through unaffected.
26
+
27
+ If the start_date of the record in the RHR is later than the end_date of the record in the LHR, the record in the LHR
28
+ is passed through unaffected.
29
+ EOF
30
+
31
+ allows_one_upstream
32
+
33
+ def query(db)
34
+ # If the RHS's min start date is less than the LHS start date,
35
+ # the entire LHS date range is truncated, which implies the row itself
36
+ # is ineligible to pass thru
37
+ ds = db.from(left_stream(db)).from_self(alias: :l)
38
+ .left_join(Sequel.as(right_stream_query(db), :r), join_columns.inject(&:&))
39
+ .where(where_criteria)
40
+
41
+ ds = dm.selectify(ds, qualifier: :l, replace: replacement_columns)
42
+
43
+ ds = apply_selectors(ds)
44
+
45
+ ds.from_self
46
+ end
47
+
48
+ private
49
+
50
+ def compare_all?
51
+ options[:compare_all]
52
+ end
53
+
54
+ def right_stream_query(db)
55
+ rh = if compare_all?
56
+ super
57
+ else
58
+ occ = to_op([ :occurrence, occurrence_number, right ])
59
+ occ.evaluate(db)
60
+ end
61
+ columnizer.apply(rh)
62
+ end
63
+
64
+ def columnizer
65
+ columnizer = Columnizer.new
66
+ columnizer.add_columns(:person_id, trim_date, *join_columns_option)
67
+ columnizer
68
+ end
69
+ end
70
+ end
71
+ end
@@ -1,55 +1,43 @@
1
- require_relative 'temporal_operator'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "trim_date"
2
4
 
3
5
  module ConceptQL
4
6
  module Operators
5
- # Trims the end_date of the LHS set of results by the RHS's earliest
7
+ # Trims the end_date of the LHS set of records by the RHS's earliest
6
8
  # start_date (per person)
7
9
  # If a the RHS contains a start_date that comes before the LHS's start_date
8
- # that LHS result is completely discarded.
10
+ # that LHS record is completely discarded.
9
11
  #
10
- # If there is no RHS result for an LHS result, the LHS result is passed
12
+ # If there is no RHS record for an LHS record, the LHS record is passed
11
13
  # thru unaffected.
12
14
  #
13
- # If the RHS result's start_date is later than the LHS end_date, the LHS
14
- # result is passed thru unaffected.
15
- class TrimDateEnd < TemporalOperator
16
- desc <<-EOF
17
- Trims the end_date of the LHS set of results by the RHS's earliest
18
- start_date (per person)
19
- If a the RHS contains a start_date that comes before the LHS's start_date
20
- that LHS result is completely discarded.
21
-
22
- If there is no RHS result for an LHS result, the LHS result is passed
23
- thru unaffected.
24
-
25
- If the RHS result's start_date is later than the LHS end_date, the LHS
26
- result is passed thru unaffected.
27
- EOF
15
+ # If the RHS record's start_date is later than the LHS end_date, the LHS
16
+ # record is passed thru unaffected.
17
+ class TrimDateEnd < TrimDate
18
+ register __FILE__
19
+
20
+ desc "For each person, trims the end_date of all left hand records by the earliest start_date in the right hand records."
21
+
28
22
  allows_one_upstream
29
- category %(Temporal Manipulation)
30
-
31
- def query(db)
32
- grouped_right = db.from(right_stream(db)).select_group(:person_id).select_append(Sequel.as(Sequel.function(:min, :start_date), :start_date))
33
-
34
- where_criteria = Sequel.expr { l__start_date <= r__start_date }
35
- where_criteria = where_criteria.|(r__start_date: nil)
36
-
37
- # If the RHS's min start date is less than the LHS start date,
38
- # the entire LHS date range is truncated, which implies the row itself
39
- # is ineligible to pass thru
40
- db.from(db.from(left_stream(db))
41
- .left_join(Sequel.as(grouped_right, :r), l__person_id: :r__person_id)
42
- .where(where_criteria)
43
- .select(*new_columns)
44
- .select_append(Sequel.as(Sequel.function(:least, :l__end_date, :r__start_date), :end_date))
45
- )
46
- end
47
23
 
48
24
  private
49
- def new_columns
50
- (COLUMNS - [:end_date]).map { |col| "l__#{col}".to_sym }
25
+
26
+ def trim_date
27
+ :start_date
28
+ end
29
+
30
+ def where_criteria
31
+ (l_start_date <= within_start) | { r_start_date => nil }
32
+ end
33
+
34
+ def replacement_columns
35
+ { end_date: Sequel.function(:least, l_end_date, Sequel.function(:coalesce, within_start, l_end_date)) }
36
+ end
37
+
38
+ def occurrence_number
39
+ 1
51
40
  end
52
41
  end
53
42
  end
54
43
  end
55
-
@@ -1,56 +1,43 @@
1
- require_relative 'temporal_operator'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "trim_date"
2
4
 
3
5
  module ConceptQL
4
6
  module Operators
5
- # Trims the start_date of the LHS set of results by the RHS's latest
7
+ # Trims the start_date of the LHS set of records by the RHS's latest
6
8
  # end_date (per person)
7
9
  # If a the RHS contains an end_date that comes after the LHS's end_date
8
- # that LHS result is completely discarded.
10
+ # that LHS record is completely discarded.
9
11
  #
10
- # If there is no RHS result for an LHS result, the LHS result is passed
12
+ # If there is no RHS record for an LHS record, the LHS record is passed
11
13
  # thru unaffected.
12
14
  #
13
- # If the RHS result's end_date is earlier than the LHS start_date, the LHS
14
- # result is passed thru unaffected.
15
- class TrimDateStart < TemporalOperator
16
- desc <<-EOF
17
- Trims the start_date of the LHS set of results by the RHS's latest
18
- end_date (per person)
19
- If a the RHS contains an end_date that comes after the LHS's end_date
20
- that LHS result is completely discarded.
21
-
22
- If there is no RHS result for an LHS result, the LHS result is passed
23
- thru unaffected.
24
-
25
- If the RHS result's end_date is earlier than the LHS start_date, the LHS
26
- result is passed thru unaffected.
27
- EOF
15
+ # If the RHS record's end_date is earlier than the LHS start_date, the LHS
16
+ # record is passed thru unaffected.
17
+ class TrimDateStart < TrimDate
18
+ register __FILE__
19
+
20
+ desc "For each person, trims the start_date of all left hand records by the most recent end_date in the right hand records."
21
+
28
22
  allows_one_upstream
29
- category %(Temporal Manipulation)
30
-
31
- def query(db)
32
- grouped_right = db.from(right_stream(db)).select_group(:person_id).select_append(Sequel.as(Sequel.function(:max, :end_date), :end_date))
33
-
34
- where_criteria = Sequel.expr { l__end_date >= r__end_date }
35
- where_criteria = where_criteria.|(r__end_date: nil)
36
-
37
- # If the RHS's min start date is less than the LHS start date,
38
- # the entire LHS date range is truncated, which implies the row itself
39
- # is ineligible to pass thru
40
- db.from(db.from(left_stream(db))
41
- .join(Sequel.as(grouped_right, :r), l__person_id: :r__person_id)
42
- .where(where_criteria)
43
- .select(*new_columns)
44
- .select_append(Sequel.as(Sequel.function(:greatest, :l__start_date, :r__end_date), :start_date))
45
- )
46
- end
47
23
 
48
24
  private
49
- def new_columns
50
- (COLUMNS - [:start_date]).map { |col| "l__#{col}".to_sym }
25
+
26
+ def trim_date
27
+ :end_date
28
+ end
29
+
30
+ def where_criteria
31
+ (l_end_date >= within_end) | { r_end_date => nil }
32
+ end
33
+
34
+ def replacement_columns
35
+ { start_date: Sequel.function(:greatest, l_start_date, Sequel.function(:coalesce, within_end, l_start_date)) }
36
+ end
37
+
38
+ def occurrence_number
39
+ -1
51
40
  end
52
41
  end
53
42
  end
54
43
  end
55
-
56
-
@@ -1,16 +1,77 @@
1
- require_relative 'pass_thru'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "pass_thru"
2
4
 
3
5
  module ConceptQL
4
6
  module Operators
5
7
  class Union < PassThru
6
- desc 'Pools sets of incoming results into a single large set of results.'
8
+ register __FILE__
9
+
10
+ class Combiner
11
+ attr_reader :combinables
12
+
13
+ def initialize(combinables)
14
+ @combinables = combinables
15
+ end
16
+
17
+ def queries(db)
18
+ combinables.group_by(&:domain).map do |domain, combos|
19
+ include_uuid = combos.any? { |c| c.options[:uuid] }
20
+ criteria = combos.map do |combo|
21
+ combo.filter_clause(db)
22
+ end.inject(&:|)
23
+
24
+ criteria = Sequel.&(vocabularies_in(combos), criteria) if combos.length > 1
25
+
26
+ table = combos.first.criterion_table
27
+ ds = db[table]
28
+ .where(criteria)
29
+ combos.first.select_it(ds, specific_table: table, domain: domain, uuid: include_uuid)
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ # Every OR'd criterion already requires its own vocabulary, so this IN
36
+ # list selects no new rows and drops none. It is explicit because a
37
+ # plain OR stopped DuckDB's Parquet filter pushdown (bead conceptql-agx).
38
+ def vocabularies_in(combos)
39
+ conditions = combos.map(&:vocabulary_condition)
40
+ columns = conditions.flat_map(&:keys).uniq
41
+ raise "Merged vocabularies use more than one vocabulary column: #{columns}" unless columns.length == 1
42
+
43
+ { columns.first => conditions.map { |c| c[columns.first] }.uniq }
44
+ end
45
+ end
46
+
47
+ def events_per_patient
48
+ upstreams.all? { |u| u.events_per_patient == :single } ? :single : :multiple
49
+ end
50
+
51
+ def multiple_vocabularies
52
+ return true if upstreams.any?(&:multiple_vocabularies)
53
+
54
+ vocabularies.length > 1
55
+ end
56
+
57
+ desc "Combines sets of incoming records into a single large set of records."
7
58
  allows_many_upstreams
8
- category 'Set Logic'
59
+ category "Combine Streams"
60
+ default_query_columns
61
+ validate_at_least_one_upstream
62
+ validate_no_arguments
9
63
 
10
64
  def query(db)
11
- values.map do |expression|
12
- expression.evaluate(db).from_self
13
- end.inject do |q, query|
65
+ combinables, individuals = upstreams.partition { |upstream| upstream.is_a?(Vocabulary) }
66
+
67
+ queries = individuals.map do |expression|
68
+ expression.evaluate(db)
69
+ end
70
+
71
+ queries += Combiner.new(combinables).queries(db)
72
+
73
+ queries.map! { |ds| ds.from_self.select(*query_cols).from_self }
74
+ queries.inject do |q, query|
14
75
  q.union(query, all: true)
15
76
  end
16
77
  end