conceptql 0.0.1

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 (111) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/CHANGELOG.md +17 -0
  4. data/Gemfile +4 -0
  5. data/Guardfile +28 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +108 -0
  8. data/Rakefile +1 -0
  9. data/bin/conceptql +5 -0
  10. data/conceptql.gemspec +30 -0
  11. data/doc/ConceptQL Specification (alpha).pdf +0 -0
  12. data/doc/diagram_0.png +0 -0
  13. data/doc/spec.md +1208 -0
  14. data/lib/conceptql/behaviors/dottable.rb +71 -0
  15. data/lib/conceptql/cli.rb +135 -0
  16. data/lib/conceptql/date_adjuster.rb +45 -0
  17. data/lib/conceptql/graph.rb +49 -0
  18. data/lib/conceptql/graph_nodifier.rb +123 -0
  19. data/lib/conceptql/logger.rb +10 -0
  20. data/lib/conceptql/nodes/after.rb +12 -0
  21. data/lib/conceptql/nodes/before.rb +11 -0
  22. data/lib/conceptql/nodes/binary_operator_node.rb +41 -0
  23. data/lib/conceptql/nodes/casting_node.rb +75 -0
  24. data/lib/conceptql/nodes/complement.rb +16 -0
  25. data/lib/conceptql/nodes/concept.rb +38 -0
  26. data/lib/conceptql/nodes/condition_type.rb +63 -0
  27. data/lib/conceptql/nodes/cpt.rb +20 -0
  28. data/lib/conceptql/nodes/date_range.rb +39 -0
  29. data/lib/conceptql/nodes/death.rb +19 -0
  30. data/lib/conceptql/nodes/during.rb +16 -0
  31. data/lib/conceptql/nodes/except.rb +11 -0
  32. data/lib/conceptql/nodes/first.rb +24 -0
  33. data/lib/conceptql/nodes/from.rb +15 -0
  34. data/lib/conceptql/nodes/gender.rb +27 -0
  35. data/lib/conceptql/nodes/hcpcs.rb +20 -0
  36. data/lib/conceptql/nodes/icd10.rb +23 -0
  37. data/lib/conceptql/nodes/icd9.rb +23 -0
  38. data/lib/conceptql/nodes/icd9_procedure.rb +20 -0
  39. data/lib/conceptql/nodes/intersect.rb +29 -0
  40. data/lib/conceptql/nodes/last.rb +24 -0
  41. data/lib/conceptql/nodes/loinc.rb +20 -0
  42. data/lib/conceptql/nodes/node.rb +71 -0
  43. data/lib/conceptql/nodes/occurrence.rb +47 -0
  44. data/lib/conceptql/nodes/pass_thru.rb +11 -0
  45. data/lib/conceptql/nodes/person.rb +25 -0
  46. data/lib/conceptql/nodes/person_filter.rb +12 -0
  47. data/lib/conceptql/nodes/place_of_service_code.rb +23 -0
  48. data/lib/conceptql/nodes/procedure_occurrence.rb +21 -0
  49. data/lib/conceptql/nodes/race.rb +23 -0
  50. data/lib/conceptql/nodes/rxnorm.rb +20 -0
  51. data/lib/conceptql/nodes/snomed.rb +19 -0
  52. data/lib/conceptql/nodes/source_vocabulary_node.rb +54 -0
  53. data/lib/conceptql/nodes/standard_vocabulary_node.rb +43 -0
  54. data/lib/conceptql/nodes/started_by.rb +16 -0
  55. data/lib/conceptql/nodes/temporal_node.rb +25 -0
  56. data/lib/conceptql/nodes/time_window.rb +54 -0
  57. data/lib/conceptql/nodes/union.rb +15 -0
  58. data/lib/conceptql/nodes/visit.rb +11 -0
  59. data/lib/conceptql/nodes/visit_occurrence.rb +26 -0
  60. data/lib/conceptql/nodifier.rb +9 -0
  61. data/lib/conceptql/query.rb +39 -0
  62. data/lib/conceptql/tree.rb +36 -0
  63. data/lib/conceptql/version.rb +3 -0
  64. data/lib/conceptql/view_maker.rb +56 -0
  65. data/lib/conceptql.rb +7 -0
  66. data/spec/conceptql/behaviors/dottable_spec.rb +111 -0
  67. data/spec/conceptql/date_adjuster_spec.rb +68 -0
  68. data/spec/conceptql/nodes/after_spec.rb +18 -0
  69. data/spec/conceptql/nodes/before_spec.rb +18 -0
  70. data/spec/conceptql/nodes/casting_node_spec.rb +73 -0
  71. data/spec/conceptql/nodes/complement_spec.rb +15 -0
  72. data/spec/conceptql/nodes/concept_spec.rb +34 -0
  73. data/spec/conceptql/nodes/condition_type_spec.rb +113 -0
  74. data/spec/conceptql/nodes/cpt_spec.rb +31 -0
  75. data/spec/conceptql/nodes/date_range_spec.rb +35 -0
  76. data/spec/conceptql/nodes/death_spec.rb +12 -0
  77. data/spec/conceptql/nodes/during_spec.rb +32 -0
  78. data/spec/conceptql/nodes/except_spec.rb +18 -0
  79. data/spec/conceptql/nodes/first_spec.rb +37 -0
  80. data/spec/conceptql/nodes/from_spec.rb +15 -0
  81. data/spec/conceptql/nodes/gender_spec.rb +29 -0
  82. data/spec/conceptql/nodes/hcpcs_spec.rb +31 -0
  83. data/spec/conceptql/nodes/icd10_spec.rb +36 -0
  84. data/spec/conceptql/nodes/icd9_procedure_spec.rb +31 -0
  85. data/spec/conceptql/nodes/icd9_spec.rb +36 -0
  86. data/spec/conceptql/nodes/intersect_spec.rb +33 -0
  87. data/spec/conceptql/nodes/last_spec.rb +38 -0
  88. data/spec/conceptql/nodes/loinc_spec.rb +31 -0
  89. data/spec/conceptql/nodes/occurrence_spec.rb +89 -0
  90. data/spec/conceptql/nodes/person_filter_spec.rb +18 -0
  91. data/spec/conceptql/nodes/person_spec.rb +12 -0
  92. data/spec/conceptql/nodes/place_of_service_code_spec.rb +26 -0
  93. data/spec/conceptql/nodes/procedure_occurrence_spec.rb +12 -0
  94. data/spec/conceptql/nodes/query_double.rb +19 -0
  95. data/spec/conceptql/nodes/race_spec.rb +23 -0
  96. data/spec/conceptql/nodes/rxnorm_spec.rb +31 -0
  97. data/spec/conceptql/nodes/snomed_spec.rb +31 -0
  98. data/spec/conceptql/nodes/source_vocabulary_node_spec.rb +37 -0
  99. data/spec/conceptql/nodes/standard_vocabulary_node_spec.rb +40 -0
  100. data/spec/conceptql/nodes/started_by_spec.rb +25 -0
  101. data/spec/conceptql/nodes/temporal_node_spec.rb +57 -0
  102. data/spec/conceptql/nodes/time_window_spec.rb +66 -0
  103. data/spec/conceptql/nodes/union_spec.rb +25 -0
  104. data/spec/conceptql/nodes/visit_occurrence_spec.rb +12 -0
  105. data/spec/conceptql/query_spec.rb +20 -0
  106. data/spec/conceptql/tree_spec.rb +54 -0
  107. data/spec/doubles/stream_for_casting_double.rb +9 -0
  108. data/spec/doubles/stream_for_occurrence_double.rb +21 -0
  109. data/spec/doubles/stream_for_temporal_double.rb +6 -0
  110. data/spec/spec_helper.rb +74 -0
  111. metadata +327 -0
@@ -0,0 +1,43 @@
1
+ require_relative 'node'
2
+
3
+ module ConceptQL
4
+ module Nodes
5
+ # A StandardVocabularyNode is a superclass for a node that represents a criterion whose column stores information associated with a standard vocabulary.
6
+ #
7
+ # If that seems confusing, then think of CPT or SNOMED criteria. That type of criterion takes a set of values that live in the OMOP concept table.
8
+ #
9
+ # My coworker came up with a nice, gneralized query that checks for matching concept_ids and matching source_code values. This class encapsulates that query.
10
+ #
11
+ # Subclasses must provide the following methods:
12
+ # * table
13
+ # * The CDM table name where the criterion will fetch its rows
14
+ # * e.g. for CPT, this would be procedure_occurrence
15
+ # * concept_column
16
+ # * Name of the column in the table that stores a concept_id related to the criterion
17
+ # * e.g. for CPT, this would be procedure_concept_id
18
+ # * vocabulary_id
19
+ # * The vocabulary ID of the source vocabulary for the criterion
20
+ # * e.g. for CPT, a value of 4 (for CPT-4)
21
+ class StandardVocabularyNode < Node
22
+ def query(db)
23
+ db.from(table_name)
24
+ .join(:vocabulary__concept___c, c__concept_id: table_concept_column)
25
+ .where(c__concept_code: values, c__vocabulary_id: vocabulary_id)
26
+ end
27
+
28
+ def types
29
+ [table]
30
+ end
31
+
32
+ private
33
+ def table_name
34
+ @table_name ||= make_table_name(table)
35
+ end
36
+
37
+ def table_concept_column
38
+ "tab__#{concept_column}".to_sym
39
+ end
40
+ end
41
+ end
42
+ end
43
+
@@ -0,0 +1,16 @@
1
+ require_relative 'temporal_node'
2
+
3
+ module ConceptQL
4
+ module Nodes
5
+ class StartedBy < TemporalNode
6
+ def where_clause
7
+ [ { l__start_date: :r__start_date } ] + \
8
+ if inclusive?
9
+ [ Proc.new { l__end_date >= r__end_date } ]
10
+ else
11
+ [ Proc.new { l__end_date > r__end_date } ]
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,25 @@
1
+ require_relative 'binary_operator_node'
2
+
3
+ module ConceptQL
4
+ module Nodes
5
+ # Base class for all temporal nodes
6
+ #
7
+ # Subclasses must implement the where_clause method which should probably return
8
+ # a proc that can be executed as a Sequel "virtual row" e.g.
9
+ # Proc.new { l.end_date < r.start_date }
10
+ class TemporalNode < BinaryOperatorNode
11
+ def query(db)
12
+ db.from(db.from(Sequel.expr(left.evaluate(db)).as(:l))
13
+ .join(Sequel.expr(right.evaluate(db)).as(:r), [:person_id])
14
+ .where(where_clause)
15
+ .select_all(:l))
16
+ end
17
+
18
+ def inclusive?
19
+ options[:inclusive]
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+
@@ -0,0 +1,54 @@
1
+ require_relative 'node'
2
+ require_relative '../date_adjuster'
3
+
4
+ module ConceptQL
5
+ module Nodes
6
+ # A TimeWindow adjusts the start_date and end_date the incoming stream by the values specified in
7
+ # the start and end arguments.
8
+ #
9
+ # Start and end take the format of ([+-]\d*[mdy])+. For example:
10
+ # 'd' => adjust date by one day. 2012-10-07 => 2012-10-08
11
+ # 'm' => adjust date by one month. 2012-10-07 => 2012-11-07
12
+ # 'y' => adjust date by one year. 2012-10-07 => 2013-10-07
13
+ # '1y' => adjust date by one year. 2012-10-07 => 2013-10-07
14
+ # '1d1y' => adjust date by one day and one year. 2012-10-07 => 2013-10-08
15
+ # '-1d' => adjust date by negative one day. 2012-10-07 => 2012-10-06
16
+ # '-1d1y' => adjust date by negative one day and positive one year. 2012-10-07 => 2013-10-06
17
+ # '', '0', nil => don't adjust date at all
18
+ #
19
+ # Both start and end arguments must be provided, but if you do not wish to adjust a date just
20
+ # pass '', '0', or nil as that argument. E.g.:
21
+ # start: 'd', end: '' # Only adjust start_date by positive 1 day and leave end_date uneffected
22
+ class TimeWindow < Node
23
+ def query(db)
24
+ db.from(stream.evaluate(db))
25
+ end
26
+
27
+ private
28
+ def date_columns
29
+ [adjusted_start_date, adjusted_end_date]
30
+ end
31
+
32
+ def adjusted_start_date
33
+ adjusted_date(:start, :start_date)
34
+ end
35
+
36
+ def adjusted_end_date
37
+ adjusted_date(:end, :end_date)
38
+ end
39
+
40
+ # NOTE: This produces PostgreSQL-specific date adjustment. I'm not yet certain how to generalize this
41
+ # or make different versions based on RDBMS
42
+ def adjusted_date(option_arg, column)
43
+ arg = options[option_arg]
44
+ arg ||= ''
45
+ return ['end_date', column].join('___').to_sym if arg.downcase == 'end'
46
+ return ['start_date', column].join('___').to_sym if arg.downcase == 'start'
47
+ DateAdjuster.new(arg).adjustments.inject(Sequel.function(:date, column)) do |sql, adjustment|
48
+ Sequel.function(:date, sql + Sequel.lit("interval '#{adjustment}'"))
49
+ end.as(column)
50
+ end
51
+ end
52
+ end
53
+ end
54
+
@@ -0,0 +1,15 @@
1
+ require_relative 'pass_thru'
2
+
3
+ module ConceptQL
4
+ module Nodes
5
+ class Union < PassThru
6
+ def query(db)
7
+ values.map do |expression|
8
+ expression.evaluate(db)
9
+ end.inject do |q, query|
10
+ q.union(query, all: true)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,11 @@
1
+ require_relative 'node'
2
+
3
+ module ConceptQL
4
+ module Nodes
5
+ class Visit < Node
6
+ def types
7
+ [:visit_occurrence]
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,26 @@
1
+ require_relative 'casting_node'
2
+
3
+ module ConceptQL
4
+ module Nodes
5
+ class VisitOccurrence < CastingNode
6
+ def my_type
7
+ :visit_occurrence
8
+ end
9
+
10
+ def i_point_at
11
+ [ :person ]
12
+ end
13
+
14
+ def these_point_at_me
15
+ %i[
16
+ condition_occurrence
17
+ drug_cost
18
+ drug_exposure
19
+ observation
20
+ procedure_cost
21
+ procedure_occurrence
22
+ ]
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,9 @@
1
+ require 'active_support/inflector'
2
+ module ConceptQL
3
+ class Nodifier
4
+ def create(type, values)
5
+ require_relative "nodes/#{type}"
6
+ "conceptQL/nodes/#{type}".camelize.constantize.new(values)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,39 @@
1
+ require 'psych'
2
+ require_relative 'tree'
3
+ require_relative 'view_maker'
4
+
5
+ module ConceptQL
6
+ class Query
7
+ attr :statement
8
+ def initialize(db, statement, tree = Tree.new)
9
+ @db = db
10
+ @statement = statement
11
+ @tree = tree
12
+ end
13
+
14
+ def query
15
+ build_query(db)
16
+ end
17
+
18
+ def execute
19
+ ensure_views
20
+ build_query(db).all
21
+ end
22
+
23
+ def types
24
+ tree.root(self).types
25
+ end
26
+
27
+ private
28
+ attr :yaml, :tree, :db
29
+
30
+ def build_query(db)
31
+ tree.root(self).evaluate(db)
32
+ end
33
+
34
+ def ensure_views
35
+ return if db.views.include?(:person_with_dates)
36
+ ViewMaker.make_views(db)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,36 @@
1
+ require_relative 'nodifier'
2
+ require 'active_support/core_ext/hash'
3
+
4
+ module ConceptQL
5
+ class Tree
6
+ attr :nodifier, :behavior
7
+ def initialize(opts = {})
8
+ @nodifier = opts.fetch(:nodifier, Nodifier.new)
9
+ @behavior = opts.fetch(:behavior, nil)
10
+ end
11
+
12
+ def root(query)
13
+ @root ||= traverse(query.statement.symbolize_keys)
14
+ end
15
+
16
+ private
17
+ def traverse(obj)
18
+ case obj
19
+ when Hash
20
+ if obj.keys.length > 1
21
+ obj = Hash[obj.map { |key, value| [ key, traverse(value) ]}]
22
+ return obj
23
+ end
24
+ type = obj.keys.first
25
+ values = traverse(obj[type])
26
+ obj = nodifier.create(type, values)
27
+ obj.extend(behavior) if behavior
28
+ obj
29
+ when Array
30
+ obj.map { |value| traverse(value) }
31
+ else
32
+ obj
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,3 @@
1
+ module ConceptQL
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,56 @@
1
+ require 'sequel'
2
+
3
+ module ConceptQL
4
+ module ViewMaker
5
+ def self.make_views(db, schema = nil)
6
+ views.each do |view, columns|
7
+ make_view(db, schema, view, columns)
8
+ end
9
+ end
10
+
11
+ def self.make_view(db, schema, view, columns)
12
+ view_name = (view.to_s + '_with_dates')
13
+ view_name = schema + '__' + view_name if schema
14
+ view_name = view_name.to_sym
15
+
16
+ table_name = view.to_s
17
+ table_name = schema + '__' + table_name if schema
18
+ table_name = table_name.to_sym
19
+
20
+ additional_columns = [Sequel.expr(columns.shift).cast(:date).as(:start_date), Sequel.expr(columns.shift).cast(:date).as(:end_date)]
21
+ unless columns.empty?
22
+ additional_columns += columns.last.map do |column_name, column_value|
23
+ Sequel.expr(column_value).as(column_name)
24
+ end
25
+ end
26
+ query = db.from(table_name).select_all.select_append(*additional_columns)
27
+ puts query.sql
28
+ db.drop_view(view_name, if_exists: true)
29
+ db.create_view(view_name, query)
30
+ end
31
+
32
+ def self.views
33
+ person_date_of_birth = assemble_date(:year_of_birth, :month_of_birth, :day_of_birth)
34
+ {
35
+ condition_occurrence: [:condition_start_date, :condition_end_date],
36
+ death: [:death_date, :death_date, { death_id: :person_id }],
37
+ drug_exposure: [:drug_exposure_start_date, :drug_exposure_end_date],
38
+ drug_cost: [nil, nil],
39
+ payer_plan_period: [:payer_plan_period_start_date, :payer_plan_period_end_date],
40
+ person: [person_date_of_birth, person_date_of_birth],
41
+ procedure_occurrence: [:procedure_date, :procedure_date],
42
+ procedure_cost: [nil, nil],
43
+ observation: [:observation_date, :observation_date],
44
+ visit_occurrence: [:visit_start_date, :visit_end_date]
45
+ }
46
+ end
47
+
48
+ def self.assemble_date(*symbols)
49
+ strings = symbols.map do |symbol|
50
+ Sequel.function(:coalesce, symbol, '01').cast(:text)
51
+ end
52
+ strings = strings.zip(['-'] * (symbols.length - 1)).flatten.compact
53
+ Sequel.function(:date, Sequel.join(strings))
54
+ end
55
+ end
56
+ end
data/lib/conceptql.rb ADDED
@@ -0,0 +1,7 @@
1
+ require "conceptql/version"
2
+ require "conceptql/logger"
3
+ require "conceptql/query"
4
+
5
+ module ConceptQL
6
+ # Your code goes here...
7
+ end
@@ -0,0 +1,111 @@
1
+ require 'spec_helper'
2
+ require 'conceptql/nodes/node'
3
+ require 'conceptql/behaviors/dottable'
4
+
5
+ class NodeDouble < ConceptQL::Nodes::Node
6
+ include ConceptQL::Behaviors::Dottable
7
+
8
+ attr_accessor :values, :options
9
+ def initialize(*values)
10
+ @values = values
11
+ @options = {}
12
+ @types = []
13
+ end
14
+ end
15
+
16
+ describe ConceptQL::Behaviors::Dottable do
17
+ before do
18
+ @obj = NodeDouble.new
19
+ @obj.must_behave_like(:node)
20
+ end
21
+
22
+ describe '#display_name' do
23
+ it 'should show just the name if no children or arguments' do
24
+ @obj.values = []
25
+ @obj.display_name.must_equal 'Node Double'
26
+ end
27
+
28
+ it 'should show name and args' do
29
+ @obj.values = [5, 10]
30
+ @obj.display_name.must_equal 'Node Double: 5, 10'
31
+ end
32
+
33
+ it 'should not include children' do
34
+ @obj.values = [::ConceptQL::Nodes::Node.new]
35
+ @obj.display_name.must_equal 'Node Double'
36
+ end
37
+ end
38
+
39
+ describe '#node_name' do
40
+ it 'should show just the name and digit if no children' do
41
+ @obj.values = [::ConceptQL::Nodes::Node.new]
42
+ @obj.node_name.must_match(/^node_double_\d+$/)
43
+ end
44
+
45
+ it 'should not show args' do
46
+ @obj.values = [5, 10]
47
+ @obj.node_name.must_match(/^node_double_\d+$/)
48
+ end
49
+
50
+ it 'should not include children' do
51
+ @obj.values = [::ConceptQL::Nodes::Node.new]
52
+ @obj.node_name.must_match(/^node_double_\d+$/)
53
+ end
54
+ end
55
+
56
+ describe '#graph_it' do
57
+ it 'should add itself as a node if no children' do
58
+ @obj.values = []
59
+ mock_graph = Minitest::Mock.new
60
+ mock_node = Minitest::Mock.new
61
+ mock_graph.expect :add_nodes, mock_node, [@obj.node_name]
62
+ mock_node.expect :[]=, nil, [:label, @obj.display_name]
63
+ mock_node.expect :[]=, nil, [:color, 'black']
64
+ @obj.graph_it(mock_graph, Sequel.mock)
65
+
66
+ mock_node.verify
67
+ mock_graph.verify
68
+ end
69
+
70
+ it 'should add its children, then link itself as a node if children' do
71
+ class MockChild < ConceptQL::Nodes::Node
72
+ include ConceptQL::Behaviors::Dottable
73
+
74
+ attr_accessor :mock
75
+ def graph_it(graph, db)
76
+ mock.graph_it(graph, db)
77
+ end
78
+
79
+ def types
80
+ mock.types
81
+ end
82
+
83
+ def link_to(mock_graph, mock_node)
84
+ mock.link_to(mock_graph, mock_node)
85
+ end
86
+ end
87
+
88
+ mock_node = Minitest::Mock.new
89
+ mock_node.expect :[]=, nil, [:label, @obj.display_name]
90
+ mock_node.expect :[]=, nil, [:color, 'black']
91
+
92
+ mock_graph = Minitest::Mock.new
93
+ mock_graph.expect :add_nodes, mock_node, [@obj.node_name]
94
+
95
+ mock_child = MockChild.new
96
+ mock_child.mock = Minitest::Mock.new
97
+ mock_child.mock.expect :graph_it, :child_node, [mock_graph, :db]
98
+ mock_child.mock.expect :link_to, nil, [mock_graph, mock_node]
99
+
100
+ mock_child.must_behave_like(:node)
101
+
102
+ @obj.values = [mock_child]
103
+
104
+ @obj.graph_it(mock_graph, :db)
105
+
106
+ mock_node.verify
107
+ mock_graph.verify
108
+ mock_child.mock.verify
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,68 @@
1
+ require 'spec_helper'
2
+ require 'conceptql/date_adjuster'
3
+
4
+ describe ConceptQL::DateAdjuster do
5
+ describe '#adjustments' do
6
+ it 'returns nothing for input of ""' do
7
+ ConceptQL::DateAdjuster.new('').adjustments.must_equal []
8
+ end
9
+
10
+ it 'returns nothing for input of "0"' do
11
+ ConceptQL::DateAdjuster.new('').adjustments.must_equal []
12
+ end
13
+
14
+ it 'returns nothing for input of nil' do
15
+ ConceptQL::DateAdjuster.new(nil).adjustments.must_equal []
16
+ end
17
+
18
+ it 'returns single day for input of "1"' do
19
+ ConceptQL::DateAdjuster.new('1').adjustments.must_equal ['1 days']
20
+ end
21
+
22
+ it 'returns 20 days for input of "20"' do
23
+ ConceptQL::DateAdjuster.new('20').adjustments.must_equal ['20 days']
24
+ end
25
+
26
+ it 'returns single day for input of "d"' do
27
+ ConceptQL::DateAdjuster.new('d').adjustments.must_equal ['1 day']
28
+ end
29
+
30
+ it 'returns single month for input of "m"' do
31
+ ConceptQL::DateAdjuster.new('m').adjustments.must_equal ['1 month']
32
+ end
33
+
34
+ it 'returns single year for input of "y"' do
35
+ ConceptQL::DateAdjuster.new('y').adjustments.must_equal ['1 year']
36
+ end
37
+
38
+ it 'returns 2 days for input of "2d"' do
39
+ ConceptQL::DateAdjuster.new('2d').adjustments.must_equal ['2 days']
40
+ end
41
+
42
+ it 'returns 2 months for input of "2m"' do
43
+ ConceptQL::DateAdjuster.new('2m').adjustments.must_equal ['2 months']
44
+ end
45
+
46
+ it 'returns 2 years for input of "2y"' do
47
+ ConceptQL::DateAdjuster.new('2y').adjustments.must_equal ['2 years']
48
+ end
49
+
50
+ it 'returns negative single day for input of "-d"' do
51
+ ConceptQL::DateAdjuster.new('-d').adjustments.must_equal ['-1 day']
52
+ end
53
+
54
+ it 'returns negative single day, positive single month for "-dm"' do
55
+ ConceptQL::DateAdjuster.new('-dm').adjustments.must_equal ['-1 day', '1 month']
56
+ end
57
+
58
+ it 'returns negative 2 days for "-2d"' do
59
+ ConceptQL::DateAdjuster.new('-2d').adjustments.must_equal ['-2 days']
60
+ end
61
+
62
+ it 'returns positive 2 days for "2d"' do
63
+ ConceptQL::DateAdjuster.new('2d').adjustments.must_equal ['2 days']
64
+ end
65
+ end
66
+ end
67
+
68
+
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+ require 'conceptql/nodes/after'
3
+ require_double('stream_for_temporal')
4
+
5
+ describe ConceptQL::Nodes::After do
6
+ it 'behaves itself' do
7
+ ConceptQL::Nodes::After.new.must_behave_like(:temporal_node)
8
+ end
9
+
10
+ subject do
11
+ ConceptQL::Nodes::After.new(left: StreamForTemporalDouble.new, right: StreamForTemporalDouble.new)
12
+ end
13
+
14
+ it 'should use proper where clause' do
15
+ subject.query(Sequel.mock).sql.must_match 'l.start_date > r.end_date'
16
+ end
17
+ end
18
+
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+ require 'conceptql/nodes/before'
3
+ require_double('stream_for_temporal')
4
+
5
+ describe ConceptQL::Nodes::Before do
6
+ it 'behaves itself' do
7
+ ConceptQL::Nodes::Before.new.must_behave_like(:temporal_node)
8
+ end
9
+
10
+ subject do
11
+ ConceptQL::Nodes::Before.new(left: StreamForTemporalDouble.new, right: StreamForTemporalDouble.new)
12
+ end
13
+
14
+ it 'should use proper where clause' do
15
+ subject.query(Sequel.mock).sql.must_match 'l.end_date < r.start_date'
16
+ end
17
+ end
18
+
@@ -0,0 +1,73 @@
1
+ require 'spec_helper'
2
+ require 'conceptql/nodes/casting_node'
3
+ require_double('stream_for_casting')
4
+
5
+ describe ConceptQL::Nodes::CastingNode do
6
+ it 'behaves itself' do
7
+ ConceptQL::Nodes::CastingNode.new.must_behave_like(:evaluator)
8
+ end
9
+
10
+ class CastingDouble < ConceptQL::Nodes::CastingNode
11
+ def my_type
12
+ :my_type
13
+ end
14
+
15
+ def i_point_at
16
+ [ :i_point1, :i_point2 ]
17
+ end
18
+
19
+ def these_point_at_me
20
+ [ :at_me1, :at_me2 ]
21
+ end
22
+ end
23
+
24
+ describe CastingDouble do
25
+ it 'must behave' do
26
+ CastingDouble.new.must_behave_like(:casting_node)
27
+ end
28
+ end
29
+
30
+ describe '#query' do
31
+ it 'uses person_ids when an uncastable type is encountered' do
32
+ stream = StreamForCastingDouble.new
33
+ stream.types = [:uncastable]
34
+ sql = CastingDouble.new(stream).query(Sequel.mock).sql
35
+ sql.must_match 'person_id IN'
36
+ sql.must_match 'GROUP BY person_id'
37
+ sql.must_match 'FROM table'
38
+ end
39
+
40
+ it 'uses person_ids when an uncastable type is included among castable types' do
41
+ stream = StreamForCastingDouble.new
42
+ stream.types = [:i_point1, :uncastable]
43
+ sql = CastingDouble.new(stream).query(Sequel.mock).sql
44
+ sql.must_match 'person_id IN'
45
+ sql.must_match 'GROUP BY person_id'
46
+ end
47
+
48
+ it 'uses castable types if possible' do
49
+ stream = StreamForCastingDouble.new
50
+ stream.types = [:i_point1]
51
+ sql = CastingDouble.new(stream).query(Sequel.mock).sql
52
+ sql.must_match 'my_type_id IN'
53
+ sql.must_match 'GROUP BY i_point1_id'
54
+ end
55
+
56
+ it 'uses and unions multiple castable types if possible' do
57
+ stream = StreamForCastingDouble.new
58
+ stream.types = [:i_point1, :at_me2]
59
+ sql = CastingDouble.new(stream).query(Sequel.mock).sql
60
+ sql.must_match 'my_type_id IN'
61
+ sql.must_match 'GROUP BY i_point1_id'
62
+ sql.must_match 'GROUP BY at_me2_id'
63
+ sql.must_match 'UNION'
64
+ end
65
+
66
+ it 'returns all rows of a table if passed the argument "true"' do
67
+ sql = CastingDouble.new(true).query(Sequel.mock).sql
68
+ sql.must_match "SELECT * FROM my_type_with_dates AS tab"
69
+ end
70
+ end
71
+ end
72
+
73
+
@@ -0,0 +1,15 @@
1
+ require 'spec_helper'
2
+ require 'conceptql/nodes/complement'
3
+ require_relative 'query_double'
4
+
5
+ describe ConceptQL::Nodes::Complement do
6
+ it 'behaves itself' do
7
+ ConceptQL::Nodes::Complement.new.must_behave_like(:evaluator)
8
+ end
9
+
10
+ it 'generates complement for single criteria' do
11
+ double1 = QueryDouble.new(1)
12
+ double1.must_behave_like(:evaluator)
13
+ ConceptQL::Nodes::Complement.new(double1).query(Sequel.mock).sql.must_equal "SELECT person_id AS person_id, CAST(NULL AS bigint) AS condition_occurrence_id, CAST(NULL AS bigint) AS death_id, CAST(NULL AS bigint) AS drug_cost_id, CAST(NULL AS bigint) AS drug_exposure_id, CAST(NULL AS bigint) AS observation_id, CAST(NULL AS bigint) AS payer_plan_period_id, CAST(NULL AS bigint) AS procedure_cost_id, CAST(NULL AS bigint) AS procedure_occurrence_id, visit_occurrence_id AS visit_occurrence_id, start_date, end_date FROM visit_occurrence_with_dates AS tab WHERE (visit_occurrence_id NOT IN (SELECT * FROM (SELECT visit_occurrence_id FROM table1) AS t1 WHERE (visit_occurrence_id IS NOT NULL)))"
14
+ end
15
+ end