conceptql 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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