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.
- checksums.yaml +5 -5
- data/CHANGELOG.md +226 -117
- data/LICENSE +21 -0
- data/README.md +35 -83
- data/bin/conceptql +2 -1
- data/bin/conceptql_utils +6 -0
- data/config/multiple_vocabularies.csv +4 -0
- data/config/provenance.yml +489 -0
- data/config/race.yml +50 -0
- data/config/vocabularies.csv +87 -0
- data/lib/conceptql/behaviors/code_lister.rb +23 -0
- data/lib/conceptql/behaviors/drugish.rb +17 -0
- data/lib/conceptql/behaviors/labish.rb +19 -0
- data/lib/conceptql/behaviors/metadatable.rb +226 -73
- data/lib/conceptql/behaviors/provenanceable.rb +26 -0
- data/lib/conceptql/behaviors/single_event.rb +11 -0
- data/lib/conceptql/behaviors/timeless.rb +11 -0
- data/lib/conceptql/behaviors/utilizable.rb +206 -0
- data/lib/conceptql/behaviors/windowable.rb +27 -0
- data/lib/conceptql/cli.rb +117 -108
- data/lib/conceptql/columnizer.rb +75 -0
- data/lib/conceptql/data_model/base.rb +462 -0
- data/lib/conceptql/data_model/gdm.rb +155 -0
- data/lib/conceptql/data_model/gdm_wide.rb +42 -0
- data/lib/conceptql/data_model/omopv4_plus.rb +12 -0
- data/lib/conceptql/data_model.rb +30 -0
- data/lib/conceptql/database.rb +214 -0
- data/lib/conceptql/date_adjuster.rb +66 -18
- data/lib/conceptql/diagram.rb +243 -0
- data/lib/conceptql/lexicon/lexicon_gdm.rb +47 -0
- data/lib/conceptql/lexicon/lexicon_no_db.rb +49 -0
- data/lib/conceptql/lexicon/lexicon_ohdsi.rb +63 -0
- data/lib/conceptql/lexicon/lexicon_strategy.rb +116 -0
- data/lib/conceptql/lexicon.rb +111 -0
- data/lib/conceptql/nodifier.rb +65 -24
- data/lib/conceptql/operators/after.rb +34 -14
- data/lib/conceptql/operators/any_overlap.rb +8 -6
- data/lib/conceptql/operators/before.rb +34 -10
- data/lib/conceptql/operators/binary_operator_operator.rb +66 -23
- data/lib/conceptql/operators/casting_operator.rb +51 -36
- data/lib/conceptql/operators/co_reported.rb +111 -0
- data/lib/conceptql/operators/concurrent_within.rb +69 -0
- data/lib/conceptql/operators/contains.rb +12 -8
- data/lib/conceptql/operators/count.rb +17 -9
- data/lib/conceptql/operators/date_range.rb +48 -14
- data/lib/conceptql/operators/death.rb +14 -4
- data/lib/conceptql/operators/dedup.rb +31 -0
- data/lib/conceptql/operators/during.rb +7 -12
- data/lib/conceptql/operators/episode.rb +146 -0
- data/lib/conceptql/operators/equal.rb +9 -3
- data/lib/conceptql/operators/ethnicity.rb +49 -0
- data/lib/conceptql/operators/except.rb +19 -12
- data/lib/conceptql/operators/filter.rb +23 -8
- data/lib/conceptql/operators/first.rb +16 -3
- data/lib/conceptql/operators/from.rb +52 -4
- data/lib/conceptql/operators/gender.rb +34 -11
- data/lib/conceptql/operators/hospice.rb +19 -0
- data/lib/conceptql/operators/hospitalization.rb +19 -0
- data/lib/conceptql/operators/information_periods.rb +36 -0
- data/lib/conceptql/operators/invalid.rb +35 -0
- data/lib/conceptql/operators/last.rb +16 -3
- data/lib/conceptql/operators/match.rb +91 -0
- data/lib/conceptql/operators/multiple_vocabularies.rb +172 -0
- data/lib/conceptql/operators/numeric.rb +65 -23
- data/lib/conceptql/operators/numeric_filter.rb +68 -0
- data/lib/conceptql/operators/occurrence.rb +153 -32
- data/lib/conceptql/operators/one_in_two_out.rb +237 -38
- data/lib/conceptql/operators/operator.rb +624 -178
- data/lib/conceptql/operators/pass_thru.rb +17 -3
- data/lib/conceptql/operators/person.rb +18 -6
- data/lib/conceptql/operators/person_filter.rb +9 -3
- data/lib/conceptql/operators/place_of_service_filter.rb +43 -0
- data/lib/conceptql/operators/provenance.rb +55 -0
- data/lib/conceptql/operators/provider_filter.rb +62 -0
- data/lib/conceptql/operators/race.rb +55 -7
- data/lib/conceptql/operators/read.rb +168 -0
- data/lib/conceptql/operators/recall.rb +49 -14
- data/lib/conceptql/operators/snf.rb +19 -0
- data/lib/conceptql/operators/sum.rb +29 -12
- data/lib/conceptql/operators/temporal_operator.rb +108 -16
- data/lib/conceptql/operators/time_window.rb +20 -32
- data/lib/conceptql/operators/trim_date.rb +71 -0
- data/lib/conceptql/operators/trim_date_end.rb +28 -40
- data/lib/conceptql/operators/trim_date_start.rb +28 -41
- data/lib/conceptql/operators/union.rb +67 -6
- data/lib/conceptql/operators/vocabulary.rb +151 -0
- data/lib/conceptql/paths.rb +41 -0
- data/lib/conceptql/query.rb +191 -26
- data/lib/conceptql/query_modifiers/gdm/admission_date_query_modifier.rb +46 -0
- data/lib/conceptql/query_modifiers/gdm/drug_query_modifier.rb +54 -0
- data/lib/conceptql/query_modifiers/gdm/lab_query_modifier.rb +57 -0
- data/lib/conceptql/query_modifiers/gdm/pos_query_modifier.rb +46 -0
- data/lib/conceptql/query_modifiers/gdm/provenance_query_modifier.rb +45 -0
- data/lib/conceptql/query_modifiers/gdm/provider_query_modifier.rb +53 -0
- data/lib/conceptql/query_modifiers/gdm_wide/admission_date_query_modifier.rb +42 -0
- data/lib/conceptql/query_modifiers/gdm_wide/drug_query_modifier.rb +52 -0
- data/lib/conceptql/query_modifiers/gdm_wide/lab_query_modifier.rb +55 -0
- data/lib/conceptql/query_modifiers/gdm_wide/pos_query_modifier.rb +37 -0
- data/lib/conceptql/query_modifiers/gdm_wide/provenance_query_modifier.rb +38 -0
- data/lib/conceptql/query_modifiers/omopv4_plus/admission_date_query_modifier.rb +45 -0
- data/lib/conceptql/query_modifiers/omopv4_plus/drug_query_modifier.rb +80 -0
- data/lib/conceptql/query_modifiers/omopv4_plus/pos_query_modifier.rb +47 -0
- data/lib/conceptql/query_modifiers/omopv4_plus/provenance_query_modifier.rb +45 -0
- data/lib/conceptql/query_modifiers/omopv4_plus/provider_query_modifier.rb +26 -0
- data/lib/conceptql/query_modifiers/query_modifier.rb +20 -0
- data/lib/conceptql/rdbms/duckdb.rb +25 -0
- data/lib/conceptql/rdbms/generic.rb +120 -0
- data/lib/conceptql/rdbms/postgres.rb +51 -0
- data/lib/conceptql/rdbms/spark.rb +22 -0
- data/lib/conceptql/rdbms.rb +22 -0
- data/lib/conceptql/scope.rb +481 -31
- data/lib/conceptql/sql_formatters/formatter.rb +25 -0
- data/lib/conceptql/sql_formatters.rb +22 -0
- data/lib/conceptql/utils.rb +113 -0
- data/lib/conceptql/utils_cli.rb +17 -0
- data/lib/conceptql/version.rb +3 -1
- data/lib/conceptql/visitors/metadata_extractor.rb +54 -0
- data/lib/conceptql/vocabularies/behaviors/costish.rb +24 -0
- data/lib/conceptql/vocabularies/behaviors/gdmish.rb +19 -0
- data/lib/conceptql/vocabularies/behaviors/omopish.rb +32 -0
- data/lib/conceptql/vocabularies/dynamic_vocabularies.rb +63 -0
- data/lib/conceptql/vocabularies/entry.rb +138 -0
- data/lib/conceptql/vocabularies/vocabulary_class_factory.rb +85 -0
- data/lib/conceptql/window/base.rb +26 -0
- data/lib/conceptql/window/date_range.rb +25 -0
- data/lib/conceptql/window/table.rb +116 -0
- data/lib/conceptql/window.rb +72 -0
- data/lib/conceptql.rb +65 -2
- data/lib/sequel/extensions/pg_ctas_explain.rb +22 -0
- data/lib/sequel/extensions/pg_vacuum_table.rb +23 -0
- data/schemas/gdm.yml +882 -0
- data/schemas/gdm_wide.yml +757 -0
- data/schemas/ohdsi_vocabs.yml +143 -0
- data/schemas/omopv4_plus.yml +729 -0
- data/schemas/omopv4_plus_more_ignorables.tsv +13 -0
- metadata +267 -207
- data/.gitignore +0 -23
- data/Gemfile +0 -4
- data/Guardfile +0 -56
- data/LICENSE.txt +0 -22
- data/Rakefile +0 -1
- data/conceptql.gemspec +0 -32
- data/doc/ConceptQL Specification (alpha).pdf +0 -0
- data/doc/diagram_0.png +0 -0
- data/doc/implementation_notes.md +0 -39
- data/doc/spec.md +0 -1344
- data/lib/conceptql/behaviors/debuggable.rb +0 -70
- data/lib/conceptql/behaviors/dottable.rb +0 -103
- data/lib/conceptql/behaviors/preppable.rb +0 -20
- data/lib/conceptql/converter.rb +0 -65
- data/lib/conceptql/debugger.rb +0 -48
- data/lib/conceptql/fake_grapher.rb +0 -20
- data/lib/conceptql/graph.rb +0 -52
- data/lib/conceptql/graph_nodifier.rb +0 -203
- data/lib/conceptql/logger.rb +0 -10
- data/lib/conceptql/operators/complement.rb +0 -38
- data/lib/conceptql/operators/concept.rb +0 -70
- data/lib/conceptql/operators/condition_type.rb +0 -71
- data/lib/conceptql/operators/cpt.rb +0 -25
- data/lib/conceptql/operators/drug_type_concept.rb +0 -21
- data/lib/conceptql/operators/from_seer_visits.rb +0 -23
- data/lib/conceptql/operators/hcpcs.rb +0 -25
- data/lib/conceptql/operators/icd10.rb +0 -28
- data/lib/conceptql/operators/icd9.rb +0 -28
- data/lib/conceptql/operators/icd9_procedure.rb +0 -25
- data/lib/conceptql/operators/intersect.rb +0 -34
- data/lib/conceptql/operators/loinc.rb +0 -25
- data/lib/conceptql/operators/medcode.rb +0 -28
- data/lib/conceptql/operators/medcode_procedure.rb +0 -27
- data/lib/conceptql/operators/ndc.rb +0 -29
- data/lib/conceptql/operators/observation_by_enttype.rb +0 -29
- data/lib/conceptql/operators/observation_period.rb +0 -30
- data/lib/conceptql/operators/overlapped_by.rb +0 -23
- data/lib/conceptql/operators/overlaps.rb +0 -19
- data/lib/conceptql/operators/place_of_service_code.rb +0 -24
- data/lib/conceptql/operators/procedure_occurrence.rb +0 -25
- data/lib/conceptql/operators/prodcode.rb +0 -29
- data/lib/conceptql/operators/rxnorm.rb +0 -24
- data/lib/conceptql/operators/snomed.rb +0 -24
- data/lib/conceptql/operators/snomed_condition.rb +0 -26
- data/lib/conceptql/operators/source_vocabulary_operator.rb +0 -58
- data/lib/conceptql/operators/standard_vocabulary_operator.rb +0 -45
- data/lib/conceptql/operators/started_by.rb +0 -22
- data/lib/conceptql/operators/to_seer_visits.rb +0 -24
- data/lib/conceptql/operators/visit.rb +0 -15
- data/lib/conceptql/operators/visit_occurrence.rb +0 -30
- data/lib/conceptql/tree.rb +0 -53
- data/lib/conceptql/utils/temp_table.rb +0 -72
- data/spec/conceptql/behaviors/dottable_spec.rb +0 -99
- data/spec/conceptql/converter_spec.rb +0 -51
- data/spec/conceptql/date_adjuster_spec.rb +0 -68
- data/spec/conceptql/operators/after_spec.rb +0 -16
- data/spec/conceptql/operators/before_spec.rb +0 -16
- data/spec/conceptql/operators/casting_operator_spec.rb +0 -68
- data/spec/conceptql/operators/complement_spec.rb +0 -15
- data/spec/conceptql/operators/concept_spec.rb +0 -40
- data/spec/conceptql/operators/condition_type_spec.rb +0 -128
- data/spec/conceptql/operators/contains_spec.rb +0 -19
- data/spec/conceptql/operators/cpt_spec.rb +0 -29
- data/spec/conceptql/operators/date_range_spec.rb +0 -33
- data/spec/conceptql/operators/death_spec.rb +0 -10
- data/spec/conceptql/operators/during_spec.rb +0 -30
- data/spec/conceptql/operators/except_spec.rb +0 -15
- data/spec/conceptql/operators/first_spec.rb +0 -35
- data/spec/conceptql/operators/from_spec.rb +0 -13
- data/spec/conceptql/operators/gender_spec.rb +0 -27
- data/spec/conceptql/operators/hcpcs_spec.rb +0 -29
- data/spec/conceptql/operators/icd10_spec.rb +0 -34
- data/spec/conceptql/operators/icd9_procedure_spec.rb +0 -29
- data/spec/conceptql/operators/icd9_spec.rb +0 -34
- data/spec/conceptql/operators/intersect_spec.rb +0 -28
- data/spec/conceptql/operators/last_spec.rb +0 -36
- data/spec/conceptql/operators/loinc_spec.rb +0 -29
- data/spec/conceptql/operators/medcode_procedure_spec.rb +0 -34
- data/spec/conceptql/operators/medcode_spec.rb +0 -34
- data/spec/conceptql/operators/observation_period_spec.rb +0 -10
- data/spec/conceptql/operators/occurrence_spec.rb +0 -87
- data/spec/conceptql/operators/overlapped_by_spec.rb +0 -32
- data/spec/conceptql/operators/overlaps_spec.rb +0 -21
- data/spec/conceptql/operators/person_filter_spec.rb +0 -15
- data/spec/conceptql/operators/person_spec.rb +0 -10
- data/spec/conceptql/operators/place_of_service_code_spec.rb +0 -24
- data/spec/conceptql/operators/procedure_occurrence_spec.rb +0 -10
- data/spec/conceptql/operators/prodcode_spec.rb +0 -35
- data/spec/conceptql/operators/query_double.rb +0 -20
- data/spec/conceptql/operators/query_double_spec.rb +0 -7
- data/spec/conceptql/operators/race_spec.rb +0 -21
- data/spec/conceptql/operators/rxnorm_spec.rb +0 -29
- data/spec/conceptql/operators/snomed_spec.rb +0 -29
- data/spec/conceptql/operators/source_vocabulary_operator_spec.rb +0 -35
- data/spec/conceptql/operators/standard_vocabulary_operator_spec.rb +0 -35
- data/spec/conceptql/operators/started_by_spec.rb +0 -22
- data/spec/conceptql/operators/temporal_operator_spec.rb +0 -51
- data/spec/conceptql/operators/time_window_spec.rb +0 -77
- data/spec/conceptql/operators/union_spec.rb +0 -21
- data/spec/conceptql/operators/visit_occurrence_spec.rb +0 -10
- data/spec/conceptql/query_spec.rb +0 -22
- data/spec/conceptql/tree_spec.rb +0 -50
- data/spec/doubles/stream_for_casting_double.rb +0 -9
- data/spec/doubles/stream_for_occurrence_double.rb +0 -25
- data/spec/doubles/stream_for_temporal_double.rb +0 -6
- data/spec/spec_helper.rb +0 -102
|
@@ -1,36 +1,128 @@
|
|
|
1
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
22
|
-
|
|
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
|
|
26
|
-
|
|
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
|
|
30
|
-
|
|
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
|
-
|
|
2
|
-
|
|
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 <
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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.
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
10
|
+
# that LHS record is completely discarded.
|
|
9
11
|
#
|
|
10
|
-
# If there is no RHS
|
|
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
|
|
14
|
-
#
|
|
15
|
-
class TrimDateEnd <
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
start_date
|
|
19
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
10
|
+
# that LHS record is completely discarded.
|
|
9
11
|
#
|
|
10
|
-
# If there is no RHS
|
|
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
|
|
14
|
-
#
|
|
15
|
-
class TrimDateStart <
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
end_date
|
|
19
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|