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.
Files changed (196) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/CHANGELOG.md +53 -1
  4. data/Guardfile +52 -24
  5. data/conceptql.gemspec +5 -3
  6. data/doc/spec.md +2 -2
  7. data/lib/conceptql/behaviors/debuggable.rb +70 -0
  8. data/lib/conceptql/behaviors/dottable.rb +28 -14
  9. data/lib/conceptql/behaviors/metadatable.rb +97 -0
  10. data/lib/conceptql/behaviors/preppable.rb +20 -0
  11. data/lib/conceptql/cli.rb +31 -5
  12. data/lib/conceptql/converter.rb +65 -0
  13. data/lib/conceptql/debugger.rb +48 -0
  14. data/lib/conceptql/graph.rb +14 -13
  15. data/lib/conceptql/graph_nodifier.rb +49 -17
  16. data/lib/conceptql/nodifier.rb +31 -6
  17. data/lib/conceptql/operators/after.rb +25 -0
  18. data/lib/conceptql/operators/any_overlap.rb +15 -0
  19. data/lib/conceptql/operators/before.rb +21 -0
  20. data/lib/conceptql/{nodes/binary_operator_node.rb → operators/binary_operator_operator.rb} +12 -8
  21. data/lib/conceptql/{nodes/casting_node.rb → operators/casting_operator.rb} +8 -7
  22. data/lib/conceptql/{nodes → operators}/complement.rb +14 -11
  23. data/lib/conceptql/operators/concept.rb +70 -0
  24. data/lib/conceptql/{nodes → operators}/condition_type.rb +13 -5
  25. data/lib/conceptql/operators/contains.rb +17 -0
  26. data/lib/conceptql/operators/count.rb +26 -0
  27. data/lib/conceptql/operators/cpt.rb +25 -0
  28. data/lib/conceptql/{nodes → operators}/date_range.rb +11 -6
  29. data/lib/conceptql/operators/death.rb +23 -0
  30. data/lib/conceptql/operators/drug_type_concept.rb +21 -0
  31. data/lib/conceptql/{nodes → operators}/during.rb +9 -3
  32. data/lib/conceptql/operators/equal.rb +13 -0
  33. data/lib/conceptql/operators/except.rb +28 -0
  34. data/lib/conceptql/operators/filter.rb +21 -0
  35. data/lib/conceptql/{nodes → operators}/first.rb +7 -5
  36. data/lib/conceptql/{nodes → operators}/from.rb +2 -2
  37. data/lib/conceptql/operators/from_seer_visits.rb +23 -0
  38. data/lib/conceptql/{nodes → operators}/gender.rb +6 -6
  39. data/lib/conceptql/operators/hcpcs.rb +25 -0
  40. data/lib/conceptql/operators/icd10.rb +28 -0
  41. data/lib/conceptql/operators/icd9.rb +28 -0
  42. data/lib/conceptql/operators/icd9_procedure.rb +25 -0
  43. data/lib/conceptql/{nodes → operators}/intersect.rb +7 -3
  44. data/lib/conceptql/{nodes → operators}/last.rb +7 -5
  45. data/lib/conceptql/operators/loinc.rb +25 -0
  46. data/lib/conceptql/operators/medcode.rb +28 -0
  47. data/lib/conceptql/operators/medcode_procedure.rb +27 -0
  48. data/lib/conceptql/operators/ndc.rb +29 -0
  49. data/lib/conceptql/operators/numeric.rb +54 -0
  50. data/lib/conceptql/operators/observation_by_enttype.rb +29 -0
  51. data/lib/conceptql/operators/observation_period.rb +30 -0
  52. data/lib/conceptql/operators/occurrence.rb +70 -0
  53. data/lib/conceptql/operators/one_in_two_out.rb +62 -0
  54. data/lib/conceptql/{nodes/node.rb → operators/operator.rb} +84 -52
  55. data/lib/conceptql/operators/overlapped_by.rb +23 -0
  56. data/lib/conceptql/operators/overlaps.rb +19 -0
  57. data/lib/conceptql/operators/pass_thru.rb +11 -0
  58. data/lib/conceptql/{nodes → operators}/person.rb +8 -4
  59. data/lib/conceptql/operators/person_filter.rb +13 -0
  60. data/lib/conceptql/{nodes → operators}/place_of_service_code.rb +7 -7
  61. data/lib/conceptql/operators/procedure_occurrence.rb +25 -0
  62. data/lib/conceptql/operators/prodcode.rb +29 -0
  63. data/lib/conceptql/{nodes → operators}/race.rb +7 -7
  64. data/lib/conceptql/operators/recall.rb +38 -0
  65. data/lib/conceptql/operators/rxnorm.rb +24 -0
  66. data/lib/conceptql/operators/snomed.rb +24 -0
  67. data/lib/conceptql/operators/snomed_condition.rb +26 -0
  68. data/lib/conceptql/{nodes/source_vocabulary_node.rb → operators/source_vocabulary_operator.rb} +7 -4
  69. data/lib/conceptql/{nodes/standard_vocabulary_node.rb → operators/standard_vocabulary_operator.rb} +6 -4
  70. data/lib/conceptql/{nodes → operators}/started_by.rb +9 -3
  71. data/lib/conceptql/{nodes → operators}/sum.rb +9 -4
  72. data/lib/conceptql/{nodes/temporal_node.rb → operators/temporal_operator.rb} +7 -4
  73. data/lib/conceptql/{nodes → operators}/time_window.rb +19 -7
  74. data/lib/conceptql/operators/to_seer_visits.rb +24 -0
  75. data/lib/conceptql/operators/trim_date_end.rb +55 -0
  76. data/lib/conceptql/operators/trim_date_start.rb +56 -0
  77. data/lib/conceptql/{nodes → operators}/union.rb +6 -2
  78. data/lib/conceptql/operators/visit.rb +15 -0
  79. data/lib/conceptql/{nodes → operators}/visit_occurrence.rb +7 -3
  80. data/lib/conceptql/query.rb +19 -17
  81. data/lib/conceptql/scope.rb +69 -0
  82. data/lib/conceptql/tree.rb +33 -18
  83. data/lib/conceptql/utils/temp_table.rb +72 -0
  84. data/lib/conceptql/version.rb +1 -1
  85. data/spec/conceptql/behaviors/dottable_spec.rb +39 -51
  86. data/spec/conceptql/converter_spec.rb +51 -0
  87. data/spec/conceptql/date_adjuster_spec.rb +15 -15
  88. data/spec/conceptql/operators/after_spec.rb +16 -0
  89. data/spec/conceptql/operators/before_spec.rb +16 -0
  90. data/spec/conceptql/{nodes/casting_node_spec.rb → operators/casting_operator_spec.rb} +16 -20
  91. data/spec/conceptql/operators/complement_spec.rb +15 -0
  92. data/spec/conceptql/operators/concept_spec.rb +40 -0
  93. data/spec/conceptql/{nodes → operators}/condition_type_spec.rb +39 -24
  94. data/spec/conceptql/operators/contains_spec.rb +19 -0
  95. data/spec/conceptql/operators/cpt_spec.rb +29 -0
  96. data/spec/conceptql/operators/date_range_spec.rb +33 -0
  97. data/spec/conceptql/operators/death_spec.rb +10 -0
  98. data/spec/conceptql/operators/during_spec.rb +30 -0
  99. data/spec/conceptql/operators/except_spec.rb +15 -0
  100. data/spec/conceptql/operators/first_spec.rb +35 -0
  101. data/spec/conceptql/operators/from_spec.rb +13 -0
  102. data/spec/conceptql/operators/gender_spec.rb +27 -0
  103. data/spec/conceptql/operators/hcpcs_spec.rb +29 -0
  104. data/spec/conceptql/operators/icd10_spec.rb +34 -0
  105. data/spec/conceptql/operators/icd9_procedure_spec.rb +29 -0
  106. data/spec/conceptql/operators/icd9_spec.rb +34 -0
  107. data/spec/conceptql/operators/intersect_spec.rb +28 -0
  108. data/spec/conceptql/operators/last_spec.rb +36 -0
  109. data/spec/conceptql/operators/loinc_spec.rb +29 -0
  110. data/spec/conceptql/operators/medcode_procedure_spec.rb +34 -0
  111. data/spec/conceptql/operators/medcode_spec.rb +34 -0
  112. data/spec/conceptql/operators/observation_period_spec.rb +10 -0
  113. data/spec/conceptql/operators/occurrence_spec.rb +87 -0
  114. data/spec/conceptql/operators/overlapped_by_spec.rb +32 -0
  115. data/spec/conceptql/operators/overlaps_spec.rb +21 -0
  116. data/spec/conceptql/operators/person_filter_spec.rb +15 -0
  117. data/spec/conceptql/operators/person_spec.rb +10 -0
  118. data/spec/conceptql/{nodes → operators}/place_of_service_code_spec.rb +6 -8
  119. data/spec/conceptql/operators/procedure_occurrence_spec.rb +10 -0
  120. data/spec/conceptql/operators/prodcode_spec.rb +35 -0
  121. data/spec/conceptql/operators/query_double.rb +20 -0
  122. data/spec/conceptql/operators/query_double_spec.rb +7 -0
  123. data/spec/conceptql/operators/race_spec.rb +21 -0
  124. data/spec/conceptql/operators/rxnorm_spec.rb +29 -0
  125. data/spec/conceptql/operators/snomed_spec.rb +29 -0
  126. data/spec/conceptql/operators/source_vocabulary_operator_spec.rb +35 -0
  127. data/spec/conceptql/operators/standard_vocabulary_operator_spec.rb +35 -0
  128. data/spec/conceptql/operators/started_by_spec.rb +22 -0
  129. data/spec/conceptql/{nodes/temporal_node_spec.rb → operators/temporal_operator_spec.rb} +11 -17
  130. data/spec/conceptql/operators/time_window_spec.rb +77 -0
  131. data/spec/conceptql/operators/union_spec.rb +21 -0
  132. data/spec/conceptql/operators/visit_occurrence_spec.rb +10 -0
  133. data/spec/conceptql/query_spec.rb +10 -9
  134. data/spec/conceptql/tree_spec.rb +24 -28
  135. data/spec/doubles/stream_for_casting_double.rb +1 -1
  136. data/spec/doubles/stream_for_occurrence_double.rb +1 -1
  137. data/spec/doubles/stream_for_temporal_double.rb +1 -1
  138. data/spec/spec_helper.rb +74 -58
  139. metadata +202 -133
  140. data/lib/conceptql/nodes/after.rb +0 -12
  141. data/lib/conceptql/nodes/before.rb +0 -11
  142. data/lib/conceptql/nodes/concept.rb +0 -38
  143. data/lib/conceptql/nodes/count.rb +0 -23
  144. data/lib/conceptql/nodes/cpt.rb +0 -20
  145. data/lib/conceptql/nodes/death.rb +0 -19
  146. data/lib/conceptql/nodes/define.rb +0 -96
  147. data/lib/conceptql/nodes/drug_type_concept.rb +0 -18
  148. data/lib/conceptql/nodes/equal.rb +0 -11
  149. data/lib/conceptql/nodes/except.rb +0 -11
  150. data/lib/conceptql/nodes/hcpcs.rb +0 -20
  151. data/lib/conceptql/nodes/icd10.rb +0 -23
  152. data/lib/conceptql/nodes/icd9.rb +0 -23
  153. data/lib/conceptql/nodes/icd9_procedure.rb +0 -20
  154. data/lib/conceptql/nodes/loinc.rb +0 -20
  155. data/lib/conceptql/nodes/numeric.rb +0 -40
  156. data/lib/conceptql/nodes/occurrence.rb +0 -49
  157. data/lib/conceptql/nodes/pass_thru.rb +0 -11
  158. data/lib/conceptql/nodes/person_filter.rb +0 -12
  159. data/lib/conceptql/nodes/procedure_occurrence.rb +0 -21
  160. data/lib/conceptql/nodes/recall.rb +0 -50
  161. data/lib/conceptql/nodes/rxnorm.rb +0 -20
  162. data/lib/conceptql/nodes/snomed.rb +0 -19
  163. data/lib/conceptql/nodes/visit.rb +0 -11
  164. data/spec/conceptql/nodes/after_spec.rb +0 -18
  165. data/spec/conceptql/nodes/before_spec.rb +0 -18
  166. data/spec/conceptql/nodes/complement_spec.rb +0 -15
  167. data/spec/conceptql/nodes/concept_spec.rb +0 -34
  168. data/spec/conceptql/nodes/cpt_spec.rb +0 -31
  169. data/spec/conceptql/nodes/date_range_spec.rb +0 -35
  170. data/spec/conceptql/nodes/death_spec.rb +0 -12
  171. data/spec/conceptql/nodes/during_spec.rb +0 -32
  172. data/spec/conceptql/nodes/except_spec.rb +0 -18
  173. data/spec/conceptql/nodes/first_spec.rb +0 -37
  174. data/spec/conceptql/nodes/from_spec.rb +0 -15
  175. data/spec/conceptql/nodes/gender_spec.rb +0 -29
  176. data/spec/conceptql/nodes/hcpcs_spec.rb +0 -31
  177. data/spec/conceptql/nodes/icd10_spec.rb +0 -36
  178. data/spec/conceptql/nodes/icd9_procedure_spec.rb +0 -31
  179. data/spec/conceptql/nodes/icd9_spec.rb +0 -36
  180. data/spec/conceptql/nodes/intersect_spec.rb +0 -33
  181. data/spec/conceptql/nodes/last_spec.rb +0 -38
  182. data/spec/conceptql/nodes/loinc_spec.rb +0 -31
  183. data/spec/conceptql/nodes/occurrence_spec.rb +0 -89
  184. data/spec/conceptql/nodes/person_filter_spec.rb +0 -18
  185. data/spec/conceptql/nodes/person_spec.rb +0 -12
  186. data/spec/conceptql/nodes/procedure_occurrence_spec.rb +0 -12
  187. data/spec/conceptql/nodes/query_double.rb +0 -19
  188. data/spec/conceptql/nodes/race_spec.rb +0 -23
  189. data/spec/conceptql/nodes/rxnorm_spec.rb +0 -31
  190. data/spec/conceptql/nodes/snomed_spec.rb +0 -31
  191. data/spec/conceptql/nodes/source_vocabulary_node_spec.rb +0 -37
  192. data/spec/conceptql/nodes/standard_vocabulary_node_spec.rb +0 -40
  193. data/spec/conceptql/nodes/started_by_spec.rb +0 -25
  194. data/spec/conceptql/nodes/time_window_spec.rb +0 -85
  195. data/spec/conceptql/nodes/union_spec.rb +0 -25
  196. 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 Nodes
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, :value_as_numeric]))
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(value_as_numeric).as(:value_as_numeric)}
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
- children.map { |c| c.evaluate(db) }.inject do |uni, q|
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 'binary_operator_node'
1
+ require_relative 'binary_operator_operator'
2
2
 
3
3
  module ConceptQL
4
- module Nodes
5
- # Base class for all temporal nodes
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 TemporalNode < BinaryOperatorNode
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 'node'
1
+ require_relative 'operator'
2
2
  require_relative '../date_adjuster'
3
3
 
4
4
  module ConceptQL
5
- module Nodes
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 < Node
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
- DateAdjuster.new(arg).adjustments.inject(Sequel.expr(column)) do |sql, (units, quantity)|
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
- Sequel.date_add(sql, units => quantity)
61
+ manipulator.date_add(sql, units => quantity)
51
62
  else
52
- Sequel.date_sub(sql, units => quantity.abs)
63
+ manipulator.date_sub(sql, units => quantity.abs)
53
64
  end
54
- end.as(column)
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 Nodes
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 'casting_node'
1
+ require_relative 'casting_operator'
2
2
 
3
3
  module ConceptQL
4
- module Nodes
5
- class VisitOccurrence < CastingNode
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
@@ -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
- queries.last
20
+ build_query(db)
19
21
  end
20
22
 
21
23
  def sql
22
- tree.opts[:sql_only] = true
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
- # To avoid a performance penalty, only execute the last
27
- # SQL statement in an array of ConceptQL statements so that define's
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 types
34
- tree.root(self).last.types
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
- nodes.map { |n| n.evaluate(db) }
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 nodes
45
- @nodes ||= tree.root(self)
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
@@ -1,38 +1,53 @@
1
1
  require_relative 'nodifier'
2
- require 'active_support/core_ext/hash'
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(*queries)
15
- @root ||= traverse(queries.flatten.map(&:statement).flatten.map(&:deep_symbolize_keys))
21
+ def root(query)
22
+ @root ||= start_traverse(query.statement)
16
23
  end
17
24
 
18
25
  private
19
- def traverse(obj)
20
- case obj
26
+
27
+ def start_traverse(stmt)
28
+ case stmt
21
29
  when Hash
22
- if obj.keys.length > 1
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
- obj.map { |value| traverse(value) }
33
- else
34
- obj
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
@@ -1,3 +1,3 @@
1
1
  module ConceptQL
2
- VERSION = "0.1.1"
2
+ VERSION = "0.2.0"
3
3
  end