conceptql 0.1.1 → 0.2.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 +4 -4
- data/.gitignore +1 -0
- data/CHANGELOG.md +53 -1
- data/Guardfile +52 -24
- data/conceptql.gemspec +5 -3
- data/doc/spec.md +2 -2
- data/lib/conceptql/behaviors/debuggable.rb +70 -0
- data/lib/conceptql/behaviors/dottable.rb +28 -14
- data/lib/conceptql/behaviors/metadatable.rb +97 -0
- data/lib/conceptql/behaviors/preppable.rb +20 -0
- data/lib/conceptql/cli.rb +31 -5
- data/lib/conceptql/converter.rb +65 -0
- data/lib/conceptql/debugger.rb +48 -0
- data/lib/conceptql/graph.rb +14 -13
- data/lib/conceptql/graph_nodifier.rb +49 -17
- data/lib/conceptql/nodifier.rb +31 -6
- data/lib/conceptql/operators/after.rb +25 -0
- data/lib/conceptql/operators/any_overlap.rb +15 -0
- data/lib/conceptql/operators/before.rb +21 -0
- data/lib/conceptql/{nodes/binary_operator_node.rb → operators/binary_operator_operator.rb} +12 -8
- data/lib/conceptql/{nodes/casting_node.rb → operators/casting_operator.rb} +8 -7
- data/lib/conceptql/{nodes → operators}/complement.rb +14 -11
- data/lib/conceptql/operators/concept.rb +70 -0
- data/lib/conceptql/{nodes → operators}/condition_type.rb +13 -5
- data/lib/conceptql/operators/contains.rb +17 -0
- data/lib/conceptql/operators/count.rb +26 -0
- data/lib/conceptql/operators/cpt.rb +25 -0
- data/lib/conceptql/{nodes → operators}/date_range.rb +11 -6
- data/lib/conceptql/operators/death.rb +23 -0
- data/lib/conceptql/operators/drug_type_concept.rb +21 -0
- data/lib/conceptql/{nodes → operators}/during.rb +9 -3
- data/lib/conceptql/operators/equal.rb +13 -0
- data/lib/conceptql/operators/except.rb +28 -0
- data/lib/conceptql/operators/filter.rb +21 -0
- data/lib/conceptql/{nodes → operators}/first.rb +7 -5
- data/lib/conceptql/{nodes → operators}/from.rb +2 -2
- data/lib/conceptql/operators/from_seer_visits.rb +23 -0
- data/lib/conceptql/{nodes → operators}/gender.rb +6 -6
- data/lib/conceptql/operators/hcpcs.rb +25 -0
- data/lib/conceptql/operators/icd10.rb +28 -0
- data/lib/conceptql/operators/icd9.rb +28 -0
- data/lib/conceptql/operators/icd9_procedure.rb +25 -0
- data/lib/conceptql/{nodes → operators}/intersect.rb +7 -3
- data/lib/conceptql/{nodes → operators}/last.rb +7 -5
- data/lib/conceptql/operators/loinc.rb +25 -0
- data/lib/conceptql/operators/medcode.rb +28 -0
- data/lib/conceptql/operators/medcode_procedure.rb +27 -0
- data/lib/conceptql/operators/ndc.rb +29 -0
- data/lib/conceptql/operators/numeric.rb +54 -0
- data/lib/conceptql/operators/observation_by_enttype.rb +29 -0
- data/lib/conceptql/operators/observation_period.rb +30 -0
- data/lib/conceptql/operators/occurrence.rb +70 -0
- data/lib/conceptql/operators/one_in_two_out.rb +62 -0
- data/lib/conceptql/{nodes/node.rb → operators/operator.rb} +84 -52
- data/lib/conceptql/operators/overlapped_by.rb +23 -0
- data/lib/conceptql/operators/overlaps.rb +19 -0
- data/lib/conceptql/operators/pass_thru.rb +11 -0
- data/lib/conceptql/{nodes → operators}/person.rb +8 -4
- data/lib/conceptql/operators/person_filter.rb +13 -0
- data/lib/conceptql/{nodes → operators}/place_of_service_code.rb +7 -7
- data/lib/conceptql/operators/procedure_occurrence.rb +25 -0
- data/lib/conceptql/operators/prodcode.rb +29 -0
- data/lib/conceptql/{nodes → operators}/race.rb +7 -7
- data/lib/conceptql/operators/recall.rb +38 -0
- data/lib/conceptql/operators/rxnorm.rb +24 -0
- data/lib/conceptql/operators/snomed.rb +24 -0
- data/lib/conceptql/operators/snomed_condition.rb +26 -0
- data/lib/conceptql/{nodes/source_vocabulary_node.rb → operators/source_vocabulary_operator.rb} +7 -4
- data/lib/conceptql/{nodes/standard_vocabulary_node.rb → operators/standard_vocabulary_operator.rb} +6 -4
- data/lib/conceptql/{nodes → operators}/started_by.rb +9 -3
- data/lib/conceptql/{nodes → operators}/sum.rb +9 -4
- data/lib/conceptql/{nodes/temporal_node.rb → operators/temporal_operator.rb} +7 -4
- data/lib/conceptql/{nodes → operators}/time_window.rb +19 -7
- data/lib/conceptql/operators/to_seer_visits.rb +24 -0
- data/lib/conceptql/operators/trim_date_end.rb +55 -0
- data/lib/conceptql/operators/trim_date_start.rb +56 -0
- data/lib/conceptql/{nodes → operators}/union.rb +6 -2
- data/lib/conceptql/operators/visit.rb +15 -0
- data/lib/conceptql/{nodes → operators}/visit_occurrence.rb +7 -3
- data/lib/conceptql/query.rb +19 -17
- data/lib/conceptql/scope.rb +69 -0
- data/lib/conceptql/tree.rb +33 -18
- data/lib/conceptql/utils/temp_table.rb +72 -0
- data/lib/conceptql/version.rb +1 -1
- data/spec/conceptql/behaviors/dottable_spec.rb +39 -51
- data/spec/conceptql/converter_spec.rb +51 -0
- data/spec/conceptql/date_adjuster_spec.rb +15 -15
- data/spec/conceptql/operators/after_spec.rb +16 -0
- data/spec/conceptql/operators/before_spec.rb +16 -0
- data/spec/conceptql/{nodes/casting_node_spec.rb → operators/casting_operator_spec.rb} +16 -20
- data/spec/conceptql/operators/complement_spec.rb +15 -0
- data/spec/conceptql/operators/concept_spec.rb +40 -0
- data/spec/conceptql/{nodes → operators}/condition_type_spec.rb +39 -24
- data/spec/conceptql/operators/contains_spec.rb +19 -0
- data/spec/conceptql/operators/cpt_spec.rb +29 -0
- data/spec/conceptql/operators/date_range_spec.rb +33 -0
- data/spec/conceptql/operators/death_spec.rb +10 -0
- data/spec/conceptql/operators/during_spec.rb +30 -0
- data/spec/conceptql/operators/except_spec.rb +15 -0
- data/spec/conceptql/operators/first_spec.rb +35 -0
- data/spec/conceptql/operators/from_spec.rb +13 -0
- data/spec/conceptql/operators/gender_spec.rb +27 -0
- data/spec/conceptql/operators/hcpcs_spec.rb +29 -0
- data/spec/conceptql/operators/icd10_spec.rb +34 -0
- data/spec/conceptql/operators/icd9_procedure_spec.rb +29 -0
- data/spec/conceptql/operators/icd9_spec.rb +34 -0
- data/spec/conceptql/operators/intersect_spec.rb +28 -0
- data/spec/conceptql/operators/last_spec.rb +36 -0
- data/spec/conceptql/operators/loinc_spec.rb +29 -0
- data/spec/conceptql/operators/medcode_procedure_spec.rb +34 -0
- data/spec/conceptql/operators/medcode_spec.rb +34 -0
- data/spec/conceptql/operators/observation_period_spec.rb +10 -0
- data/spec/conceptql/operators/occurrence_spec.rb +87 -0
- data/spec/conceptql/operators/overlapped_by_spec.rb +32 -0
- data/spec/conceptql/operators/overlaps_spec.rb +21 -0
- data/spec/conceptql/operators/person_filter_spec.rb +15 -0
- data/spec/conceptql/operators/person_spec.rb +10 -0
- data/spec/conceptql/{nodes → operators}/place_of_service_code_spec.rb +6 -8
- data/spec/conceptql/operators/procedure_occurrence_spec.rb +10 -0
- data/spec/conceptql/operators/prodcode_spec.rb +35 -0
- data/spec/conceptql/operators/query_double.rb +20 -0
- data/spec/conceptql/operators/query_double_spec.rb +7 -0
- data/spec/conceptql/operators/race_spec.rb +21 -0
- data/spec/conceptql/operators/rxnorm_spec.rb +29 -0
- data/spec/conceptql/operators/snomed_spec.rb +29 -0
- data/spec/conceptql/operators/source_vocabulary_operator_spec.rb +35 -0
- data/spec/conceptql/operators/standard_vocabulary_operator_spec.rb +35 -0
- data/spec/conceptql/operators/started_by_spec.rb +22 -0
- data/spec/conceptql/{nodes/temporal_node_spec.rb → operators/temporal_operator_spec.rb} +11 -17
- data/spec/conceptql/operators/time_window_spec.rb +77 -0
- data/spec/conceptql/operators/union_spec.rb +21 -0
- data/spec/conceptql/operators/visit_occurrence_spec.rb +10 -0
- data/spec/conceptql/query_spec.rb +10 -9
- data/spec/conceptql/tree_spec.rb +24 -28
- data/spec/doubles/stream_for_casting_double.rb +1 -1
- data/spec/doubles/stream_for_occurrence_double.rb +1 -1
- data/spec/doubles/stream_for_temporal_double.rb +1 -1
- data/spec/spec_helper.rb +74 -58
- metadata +202 -133
- data/lib/conceptql/nodes/after.rb +0 -12
- data/lib/conceptql/nodes/before.rb +0 -11
- data/lib/conceptql/nodes/concept.rb +0 -38
- data/lib/conceptql/nodes/count.rb +0 -23
- data/lib/conceptql/nodes/cpt.rb +0 -20
- data/lib/conceptql/nodes/death.rb +0 -19
- data/lib/conceptql/nodes/define.rb +0 -96
- data/lib/conceptql/nodes/drug_type_concept.rb +0 -18
- data/lib/conceptql/nodes/equal.rb +0 -11
- data/lib/conceptql/nodes/except.rb +0 -11
- data/lib/conceptql/nodes/hcpcs.rb +0 -20
- data/lib/conceptql/nodes/icd10.rb +0 -23
- data/lib/conceptql/nodes/icd9.rb +0 -23
- data/lib/conceptql/nodes/icd9_procedure.rb +0 -20
- data/lib/conceptql/nodes/loinc.rb +0 -20
- data/lib/conceptql/nodes/numeric.rb +0 -40
- data/lib/conceptql/nodes/occurrence.rb +0 -49
- data/lib/conceptql/nodes/pass_thru.rb +0 -11
- data/lib/conceptql/nodes/person_filter.rb +0 -12
- data/lib/conceptql/nodes/procedure_occurrence.rb +0 -21
- data/lib/conceptql/nodes/recall.rb +0 -50
- data/lib/conceptql/nodes/rxnorm.rb +0 -20
- data/lib/conceptql/nodes/snomed.rb +0 -19
- data/lib/conceptql/nodes/visit.rb +0 -11
- data/spec/conceptql/nodes/after_spec.rb +0 -18
- data/spec/conceptql/nodes/before_spec.rb +0 -18
- data/spec/conceptql/nodes/complement_spec.rb +0 -15
- data/spec/conceptql/nodes/concept_spec.rb +0 -34
- data/spec/conceptql/nodes/cpt_spec.rb +0 -31
- data/spec/conceptql/nodes/date_range_spec.rb +0 -35
- data/spec/conceptql/nodes/death_spec.rb +0 -12
- data/spec/conceptql/nodes/during_spec.rb +0 -32
- data/spec/conceptql/nodes/except_spec.rb +0 -18
- data/spec/conceptql/nodes/first_spec.rb +0 -37
- data/spec/conceptql/nodes/from_spec.rb +0 -15
- data/spec/conceptql/nodes/gender_spec.rb +0 -29
- data/spec/conceptql/nodes/hcpcs_spec.rb +0 -31
- data/spec/conceptql/nodes/icd10_spec.rb +0 -36
- data/spec/conceptql/nodes/icd9_procedure_spec.rb +0 -31
- data/spec/conceptql/nodes/icd9_spec.rb +0 -36
- data/spec/conceptql/nodes/intersect_spec.rb +0 -33
- data/spec/conceptql/nodes/last_spec.rb +0 -38
- data/spec/conceptql/nodes/loinc_spec.rb +0 -31
- data/spec/conceptql/nodes/occurrence_spec.rb +0 -89
- data/spec/conceptql/nodes/person_filter_spec.rb +0 -18
- data/spec/conceptql/nodes/person_spec.rb +0 -12
- data/spec/conceptql/nodes/procedure_occurrence_spec.rb +0 -12
- data/spec/conceptql/nodes/query_double.rb +0 -19
- data/spec/conceptql/nodes/race_spec.rb +0 -23
- data/spec/conceptql/nodes/rxnorm_spec.rb +0 -31
- data/spec/conceptql/nodes/snomed_spec.rb +0 -31
- data/spec/conceptql/nodes/source_vocabulary_node_spec.rb +0 -37
- data/spec/conceptql/nodes/standard_vocabulary_node_spec.rb +0 -40
- data/spec/conceptql/nodes/started_by_spec.rb +0 -25
- data/spec/conceptql/nodes/time_window_spec.rb +0 -85
- data/spec/conceptql/nodes/union_spec.rb +0 -25
- data/spec/conceptql/nodes/visit_occurrence_spec.rb +0 -12
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
require_relative 'pass_thru'
|
|
2
2
|
|
|
3
3
|
module ConceptQL
|
|
4
|
-
module
|
|
4
|
+
module Operators
|
|
5
5
|
class Sum < PassThru
|
|
6
|
+
desc <<-EOF
|
|
7
|
+
Sums value_as_number across all results that match on all but start_date, end_date.
|
|
8
|
+
For start_date and end_date the min and max of each respectively is returned.'
|
|
9
|
+
EOF
|
|
10
|
+
|
|
6
11
|
def query(db)
|
|
7
12
|
db.from(unioned(db))
|
|
8
|
-
.select_group(*(COLUMNS - [:start_date, :end_date, :criterion_id, :
|
|
13
|
+
.select_group(*(COLUMNS - [:start_date, :end_date, :criterion_id, :value_as_number]))
|
|
9
14
|
.select_append(Sequel.lit('?', 0).as(:criterion_id))
|
|
10
15
|
.select_append{ min(start_date).as(:start_date) }
|
|
11
16
|
.select_append{ max(end_date).as(:end_date) }
|
|
12
|
-
.select_append{sum(
|
|
17
|
+
.select_append{sum(value_as_number).as(:value_as_number)}
|
|
13
18
|
.from_self
|
|
14
19
|
end
|
|
15
20
|
|
|
16
21
|
def unioned(db)
|
|
17
|
-
|
|
22
|
+
upstreams.map { |c| c.evaluate(db) }.inject do |uni, q|
|
|
18
23
|
uni.union(q)
|
|
19
24
|
end
|
|
20
25
|
end
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
require_relative '
|
|
1
|
+
require_relative 'binary_operator_operator'
|
|
2
2
|
|
|
3
3
|
module ConceptQL
|
|
4
|
-
module
|
|
5
|
-
# Base class for all temporal
|
|
4
|
+
module Operators
|
|
5
|
+
# Base class for all temporal operators
|
|
6
6
|
#
|
|
7
7
|
# Subclasses must implement the where_clause method which should probably return
|
|
8
8
|
# a proc that can be executed as a Sequel "virtual row" e.g.
|
|
9
9
|
# Proc.new { l.end_date < r.start_date }
|
|
10
|
-
class
|
|
10
|
+
class TemporalOperator < BinaryOperatorOperator
|
|
11
|
+
reset_categories
|
|
12
|
+
category %w(Temporal Relative)
|
|
13
|
+
|
|
11
14
|
def query(db)
|
|
12
15
|
db.from(db.from(left_stream(db))
|
|
13
16
|
.join(right_stream(db), l__person_id: :r__person_id)
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
require_relative '
|
|
1
|
+
require_relative 'operator'
|
|
2
2
|
require_relative '../date_adjuster'
|
|
3
3
|
|
|
4
4
|
module ConceptQL
|
|
5
|
-
module
|
|
5
|
+
module Operators
|
|
6
6
|
# A TimeWindow adjusts the start_date and end_date the incoming stream by the values specified in
|
|
7
7
|
# the start and end arguments.
|
|
8
8
|
#
|
|
@@ -19,7 +19,13 @@ module ConceptQL
|
|
|
19
19
|
# Both start and end arguments must be provided, but if you do not wish to adjust a date just
|
|
20
20
|
# pass '', '0', or nil as that argument. E.g.:
|
|
21
21
|
# start: 'd', end: '' # Only adjust start_date by positive 1 day and leave end_date uneffected
|
|
22
|
-
class TimeWindow <
|
|
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
|
|
26
|
+
allows_one_upstream
|
|
27
|
+
category %(Temporal Manipulation)
|
|
28
|
+
|
|
23
29
|
def query(db)
|
|
24
30
|
db.extension :date_arithmetic
|
|
25
31
|
db.from(stream.evaluate(db))
|
|
@@ -38,6 +44,10 @@ module ConceptQL
|
|
|
38
44
|
adjusted_date(:end, :end_date)
|
|
39
45
|
end
|
|
40
46
|
|
|
47
|
+
def manipulator
|
|
48
|
+
@manipulator ||= options.fetch(:date_manipulator, Sequel)
|
|
49
|
+
end
|
|
50
|
+
|
|
41
51
|
# NOTE: This produces PostgreSQL-specific date adjustment. I'm not yet certain how to generalize this
|
|
42
52
|
# or make different versions based on RDBMS
|
|
43
53
|
def adjusted_date(option_arg, column)
|
|
@@ -45,13 +55,15 @@ module ConceptQL
|
|
|
45
55
|
arg ||= ''
|
|
46
56
|
return ['end_date', column].join('___').to_sym if arg.downcase == 'end'
|
|
47
57
|
return ['start_date', column].join('___').to_sym if arg.downcase == 'start'
|
|
48
|
-
|
|
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)|
|
|
49
60
|
if quantity > 0
|
|
50
|
-
|
|
61
|
+
manipulator.date_add(sql, units => quantity)
|
|
51
62
|
else
|
|
52
|
-
|
|
63
|
+
manipulator.date_sub(sql, units => quantity.abs)
|
|
53
64
|
end
|
|
54
|
-
end
|
|
65
|
+
end
|
|
66
|
+
Sequel.cast(adjusted_date, Date).as(column)
|
|
55
67
|
end
|
|
56
68
|
end
|
|
57
69
|
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
require_relative 'operator'
|
|
2
|
+
|
|
3
|
+
module ConceptQL
|
|
4
|
+
module Operators
|
|
5
|
+
class ToSeerVisits < Operator
|
|
6
|
+
def type
|
|
7
|
+
:visit_occurrence
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def query(db)
|
|
11
|
+
query = options.map do |k, v|
|
|
12
|
+
next if v.nil?
|
|
13
|
+
db[:observation]
|
|
14
|
+
.where(observation_source_value: k.to_s.upcase, value_as_string: v)
|
|
15
|
+
.select(:visit_occurrence_id)
|
|
16
|
+
.from_self
|
|
17
|
+
end.compact.inject { |q, i| i.intersect(q) }
|
|
18
|
+
db[:visit_occurrence].where(visit_occurrence_id: query)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
require_relative 'temporal_operator'
|
|
2
|
+
|
|
3
|
+
module ConceptQL
|
|
4
|
+
module Operators
|
|
5
|
+
# Trims the end_date of the LHS set of results by the RHS's earliest
|
|
6
|
+
# start_date (per person)
|
|
7
|
+
# If a the RHS contains a start_date that comes before the LHS's start_date
|
|
8
|
+
# that LHS result is completely discarded.
|
|
9
|
+
#
|
|
10
|
+
# If there is no RHS result for an LHS result, the LHS result is passed
|
|
11
|
+
# thru unaffected.
|
|
12
|
+
#
|
|
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
|
|
28
|
+
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
|
+
|
|
48
|
+
private
|
|
49
|
+
def new_columns
|
|
50
|
+
(COLUMNS - [:end_date]).map { |col| "l__#{col}".to_sym }
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
require_relative 'temporal_operator'
|
|
2
|
+
|
|
3
|
+
module ConceptQL
|
|
4
|
+
module Operators
|
|
5
|
+
# Trims the start_date of the LHS set of results by the RHS's latest
|
|
6
|
+
# end_date (per person)
|
|
7
|
+
# If a the RHS contains an end_date that comes after the LHS's end_date
|
|
8
|
+
# that LHS result is completely discarded.
|
|
9
|
+
#
|
|
10
|
+
# If there is no RHS result for an LHS result, the LHS result is passed
|
|
11
|
+
# thru unaffected.
|
|
12
|
+
#
|
|
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
|
|
28
|
+
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
|
+
|
|
48
|
+
private
|
|
49
|
+
def new_columns
|
|
50
|
+
(COLUMNS - [:start_date]).map { |col| "l__#{col}".to_sym }
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
require_relative 'pass_thru'
|
|
2
2
|
|
|
3
3
|
module ConceptQL
|
|
4
|
-
module
|
|
4
|
+
module Operators
|
|
5
5
|
class Union < PassThru
|
|
6
|
+
desc 'Pools sets of incoming results into a single large set of results.'
|
|
7
|
+
allows_many_upstreams
|
|
8
|
+
category 'Set Logic'
|
|
9
|
+
|
|
6
10
|
def query(db)
|
|
7
11
|
values.map do |expression|
|
|
8
|
-
expression.evaluate(db)
|
|
12
|
+
expression.evaluate(db).from_self
|
|
9
13
|
end.inject do |q, query|
|
|
10
14
|
q.union(query, all: true)
|
|
11
15
|
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
require_relative 'operator'
|
|
2
|
+
|
|
3
|
+
module ConceptQL
|
|
4
|
+
module Operators
|
|
5
|
+
class Visit < Operator
|
|
6
|
+
desc 'Generates all visit_occurrence records, or, if fed a stream, fetches all visit_occurrence records for the people represented in the incoming result set.'
|
|
7
|
+
types :visit_occurrence
|
|
8
|
+
allows_one_upstream
|
|
9
|
+
|
|
10
|
+
def types
|
|
11
|
+
[:visit_occurrence]
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
require_relative '
|
|
1
|
+
require_relative 'casting_operator'
|
|
2
2
|
|
|
3
3
|
module ConceptQL
|
|
4
|
-
module
|
|
5
|
-
class VisitOccurrence <
|
|
4
|
+
module Operators
|
|
5
|
+
class VisitOccurrence < CastingOperator
|
|
6
|
+
desc 'Returns all visits in the database, or if given a upstream, converts all results to the set of visit_occurrences related to those results.'
|
|
7
|
+
allows_one_upstream
|
|
8
|
+
types :visit_occurrence
|
|
9
|
+
|
|
6
10
|
def my_type
|
|
7
11
|
:visit_occurrence
|
|
8
12
|
end
|
data/lib/conceptql/query.rb
CHANGED
|
@@ -1,48 +1,50 @@
|
|
|
1
1
|
require 'psych'
|
|
2
|
+
require 'forwardable'
|
|
3
|
+
require_relative 'behaviors/preppable'
|
|
2
4
|
require_relative 'tree'
|
|
3
5
|
|
|
4
6
|
module ConceptQL
|
|
5
7
|
class Query
|
|
8
|
+
extend Forwardable
|
|
9
|
+
def_delegators :prepped_query, :all, :count, :execute, :order
|
|
10
|
+
|
|
6
11
|
attr :statement
|
|
7
12
|
def initialize(db, statement, tree = Tree.new)
|
|
8
13
|
@db = db
|
|
14
|
+
@db.extend_datasets(ConceptQL::Behaviors::Preppable)
|
|
9
15
|
@statement = statement
|
|
10
16
|
@tree = tree
|
|
11
17
|
end
|
|
12
18
|
|
|
13
|
-
def queries
|
|
14
|
-
build_query(db)
|
|
15
|
-
end
|
|
16
|
-
|
|
17
19
|
def query
|
|
18
|
-
|
|
20
|
+
build_query(db)
|
|
19
21
|
end
|
|
20
22
|
|
|
21
23
|
def sql
|
|
22
|
-
tree.
|
|
23
|
-
nodes.map { |node| node.sql(db) }.join(";\n") + ';'
|
|
24
|
+
(tree.scope.sql(db) << operator.sql(db)).join(";\n\n") + ';'
|
|
24
25
|
end
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
# "create_table" SQL isn't executed twice
|
|
29
|
-
def execute
|
|
30
|
-
query.all
|
|
27
|
+
def types
|
|
28
|
+
tree.root(self).types
|
|
31
29
|
end
|
|
32
30
|
|
|
33
|
-
def
|
|
34
|
-
tree.root(self)
|
|
31
|
+
def operator
|
|
32
|
+
@operator ||= tree.root(self)
|
|
35
33
|
end
|
|
36
34
|
|
|
37
35
|
private
|
|
38
36
|
attr :yaml, :tree, :db
|
|
39
37
|
|
|
40
38
|
def build_query(db)
|
|
41
|
-
|
|
39
|
+
operator.evaluate(db).tap { |q| q.prep_proc = prep_proc }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def prep_proc
|
|
43
|
+
@prep_proc = Proc.new { puts 'PREPPING'; tree.scope.prep(db) }
|
|
42
44
|
end
|
|
43
45
|
|
|
44
|
-
def
|
|
45
|
-
|
|
46
|
+
def prepped_query
|
|
47
|
+
query
|
|
46
48
|
end
|
|
47
49
|
end
|
|
48
50
|
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
require_relative 'utils/temp_table'
|
|
2
|
+
module ConceptQL
|
|
3
|
+
# Scope coordinates the creation of any TempTables that might
|
|
4
|
+
# be used when a Recall operator is present in the statement.
|
|
5
|
+
#
|
|
6
|
+
# Any time an operator is given a label, it becomes a candidate
|
|
7
|
+
# for a Recall operator to reuse the output of that operator
|
|
8
|
+
# somewhere else in the statement.
|
|
9
|
+
#
|
|
10
|
+
# Scope keeps track of all labeled operators and provides an
|
|
11
|
+
# API for Recall operators to fetch the results/types from
|
|
12
|
+
# labeled operators.
|
|
13
|
+
class Scope
|
|
14
|
+
attr_accessor :person_ids
|
|
15
|
+
attr :known_operators
|
|
16
|
+
def initialize
|
|
17
|
+
@known_operators = {}
|
|
18
|
+
@flagged = {}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def add_operator(operator)
|
|
22
|
+
# Recall operators respond to source, so when we add such an
|
|
23
|
+
# operator, we want to flag it so that we can come back to it later
|
|
24
|
+
# and create the appropriate temp tables
|
|
25
|
+
if operator.respond_to?(:source)
|
|
26
|
+
flagged[operator.source] = true
|
|
27
|
+
end
|
|
28
|
+
return unless operator.label
|
|
29
|
+
known_operators[operator.label] = operator
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def from(db, label)
|
|
33
|
+
temp_table(db, label).from(db)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def types(label)
|
|
37
|
+
fetch_operator(label).types
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def sql(db)
|
|
41
|
+
temp_tables(db).values.map do |temp|
|
|
42
|
+
temp.sql(db)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def prep(db)
|
|
47
|
+
temp_tables(db).values.map do |temp|
|
|
48
|
+
temp.build(db)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
attr :flagged
|
|
54
|
+
|
|
55
|
+
def temp_tables(db)
|
|
56
|
+
Hash[flagged.keys.map do |label|
|
|
57
|
+
[label, TempTable.new(fetch_operator(label), db)]
|
|
58
|
+
end]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def temp_table(db, label)
|
|
62
|
+
temp_tables(db)[label]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def fetch_operator(label)
|
|
66
|
+
known_operators[label] || raise("No operator with label: '#{label}'")
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
data/lib/conceptql/tree.rb
CHANGED
|
@@ -1,38 +1,53 @@
|
|
|
1
1
|
require_relative 'nodifier'
|
|
2
|
-
|
|
2
|
+
require_relative 'converter'
|
|
3
|
+
require_relative 'scope'
|
|
4
|
+
require 'facets/hash/deep_rekey'
|
|
5
|
+
require 'facets/array/recurse'
|
|
3
6
|
|
|
4
7
|
module ConceptQL
|
|
8
|
+
# Tree is used to walk through a ConceptQL statement, instantiate
|
|
9
|
+
# all operators, and then provide access to the root operator
|
|
5
10
|
class Tree
|
|
6
|
-
attr :nodifier, :behavior, :defined, :opts
|
|
11
|
+
attr :nodifier, :behavior, :defined, :opts, :temp_tables, :scope
|
|
7
12
|
def initialize(opts = {})
|
|
8
13
|
@nodifier = opts.fetch(:nodifier, Nodifier.new)
|
|
9
14
|
@behavior = opts.fetch(:behavior, nil)
|
|
10
15
|
@defined = {}
|
|
16
|
+
@temp_tables = {}
|
|
11
17
|
@opts = {}
|
|
18
|
+
@scope = opts.fetch(:scope, Scope.new)
|
|
12
19
|
end
|
|
13
20
|
|
|
14
|
-
def root(
|
|
15
|
-
@root ||=
|
|
21
|
+
def root(query)
|
|
22
|
+
@root ||= start_traverse(query.statement)
|
|
16
23
|
end
|
|
17
24
|
|
|
18
25
|
private
|
|
19
|
-
|
|
20
|
-
|
|
26
|
+
|
|
27
|
+
def start_traverse(stmt)
|
|
28
|
+
case stmt
|
|
21
29
|
when Hash
|
|
22
|
-
|
|
23
|
-
obj = Hash[obj.map { |key, value| [ key, traverse(value) ]}]
|
|
24
|
-
return obj
|
|
25
|
-
end
|
|
26
|
-
type = obj.keys.first
|
|
27
|
-
values = traverse(obj[type])
|
|
28
|
-
obj = nodifier.create(type, values, self)
|
|
29
|
-
obj.extend(behavior) if behavior
|
|
30
|
-
obj
|
|
30
|
+
traverse(converter.convert(stmt))
|
|
31
31
|
when Array
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
traverse(stmt)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def traverse(stmt)
|
|
37
|
+
stmt.recurse(Array, Hash) do |arr_or_hash|
|
|
38
|
+
if arr_or_hash.is_a?(Array)
|
|
39
|
+
type = arr_or_hash.shift
|
|
40
|
+
obj = nodifier.create(scope, type, *arr_or_hash)
|
|
41
|
+
obj.extend(behavior) if behavior
|
|
42
|
+
obj
|
|
43
|
+
else
|
|
44
|
+
arr_or_hash
|
|
45
|
+
end
|
|
35
46
|
end
|
|
36
47
|
end
|
|
48
|
+
|
|
49
|
+
def converter
|
|
50
|
+
@converter ||= Converter.new
|
|
51
|
+
end
|
|
37
52
|
end
|
|
38
53
|
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
require 'zlib'
|
|
2
|
+
|
|
3
|
+
module ConceptQL
|
|
4
|
+
# TempTable coordinates the creation of any temporary tables a statement
|
|
5
|
+
# might need.
|
|
6
|
+
#
|
|
7
|
+
# Currently, temp tables are used to share a set of results when a Recall
|
|
8
|
+
# operator is present in a statement.
|
|
9
|
+
#
|
|
10
|
+
# It also provides an API to generate the SQL that Sequel would use to create
|
|
11
|
+
# and populate the temp table
|
|
12
|
+
class TempTable
|
|
13
|
+
attr :operator
|
|
14
|
+
def initialize(operator, db)
|
|
15
|
+
@operator = operator
|
|
16
|
+
fake_it!(db)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def build(db)
|
|
20
|
+
@built ||= build_it(db)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def sql(db)
|
|
24
|
+
# Sequel doesn't (currently) provide an API for getting the SQL for the
|
|
25
|
+
# creation of tables, so we're calling one of the private methods
|
|
26
|
+
sql = db[db.send(:create_table_as_sql, table_name, operator.evaluate(db), temp: true)].sql
|
|
27
|
+
["-- #{operator.label}", sql].join("\n")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def from(db)
|
|
31
|
+
db[table_name]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def table_name
|
|
37
|
+
namify(operator.label)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def build_it(db)
|
|
41
|
+
db.create_table!(table_name, as: operator.evaluate(db), temp: true)
|
|
42
|
+
true
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def namify(name)
|
|
46
|
+
digest = Zlib.crc32 name
|
|
47
|
+
('_' + digest.to_s).to_sym
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Creates the temp table, but populates it with a single fake row of null
|
|
51
|
+
# values. Some operations, such as generating the SQL for a statement
|
|
52
|
+
# Want the temp tables to exist, but we don't _really_ need the actual
|
|
53
|
+
# results in the temp table since they might take a long time to calculate.
|
|
54
|
+
def fake_it!(db)
|
|
55
|
+
db.create_table!(table_name, temp: true, as: fake_row(db))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def fake_row(db)
|
|
59
|
+
db
|
|
60
|
+
.select(Sequel.cast(nil, Bignum).as(:person_id))
|
|
61
|
+
.select_append(Sequel.cast(nil, Bignum).as(:criterion_id))
|
|
62
|
+
.select_append(Sequel.cast(nil, String).as(:criterion_type))
|
|
63
|
+
.select_append(Sequel.cast(nil, Date).as(:start_date))
|
|
64
|
+
.select_append(Sequel.cast(nil, Date).as(:end_date))
|
|
65
|
+
.select_append(Sequel.cast(nil, Bignum).as(:value_as_number))
|
|
66
|
+
.select_append(Sequel.cast(nil, String).as(:value_as_string))
|
|
67
|
+
.select_append(Sequel.cast(nil, Bignum).as(:value_as_concept_id))
|
|
68
|
+
.select_append(Sequel.cast(nil, String).as(:units_source_value))
|
|
69
|
+
.select_append(Sequel.cast(nil, String).as(:source_value))
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
data/lib/conceptql/version.rb
CHANGED