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.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/CHANGELOG.md +17 -0
- data/Gemfile +4 -0
- data/Guardfile +28 -0
- data/LICENSE.txt +22 -0
- data/README.md +108 -0
- data/Rakefile +1 -0
- data/bin/conceptql +5 -0
- data/conceptql.gemspec +30 -0
- data/doc/ConceptQL Specification (alpha).pdf +0 -0
- data/doc/diagram_0.png +0 -0
- data/doc/spec.md +1208 -0
- data/lib/conceptql/behaviors/dottable.rb +71 -0
- data/lib/conceptql/cli.rb +135 -0
- data/lib/conceptql/date_adjuster.rb +45 -0
- data/lib/conceptql/graph.rb +49 -0
- data/lib/conceptql/graph_nodifier.rb +123 -0
- data/lib/conceptql/logger.rb +10 -0
- data/lib/conceptql/nodes/after.rb +12 -0
- data/lib/conceptql/nodes/before.rb +11 -0
- data/lib/conceptql/nodes/binary_operator_node.rb +41 -0
- data/lib/conceptql/nodes/casting_node.rb +75 -0
- data/lib/conceptql/nodes/complement.rb +16 -0
- data/lib/conceptql/nodes/concept.rb +38 -0
- data/lib/conceptql/nodes/condition_type.rb +63 -0
- data/lib/conceptql/nodes/cpt.rb +20 -0
- data/lib/conceptql/nodes/date_range.rb +39 -0
- data/lib/conceptql/nodes/death.rb +19 -0
- data/lib/conceptql/nodes/during.rb +16 -0
- data/lib/conceptql/nodes/except.rb +11 -0
- data/lib/conceptql/nodes/first.rb +24 -0
- data/lib/conceptql/nodes/from.rb +15 -0
- data/lib/conceptql/nodes/gender.rb +27 -0
- data/lib/conceptql/nodes/hcpcs.rb +20 -0
- data/lib/conceptql/nodes/icd10.rb +23 -0
- data/lib/conceptql/nodes/icd9.rb +23 -0
- data/lib/conceptql/nodes/icd9_procedure.rb +20 -0
- data/lib/conceptql/nodes/intersect.rb +29 -0
- data/lib/conceptql/nodes/last.rb +24 -0
- data/lib/conceptql/nodes/loinc.rb +20 -0
- data/lib/conceptql/nodes/node.rb +71 -0
- data/lib/conceptql/nodes/occurrence.rb +47 -0
- data/lib/conceptql/nodes/pass_thru.rb +11 -0
- data/lib/conceptql/nodes/person.rb +25 -0
- data/lib/conceptql/nodes/person_filter.rb +12 -0
- data/lib/conceptql/nodes/place_of_service_code.rb +23 -0
- data/lib/conceptql/nodes/procedure_occurrence.rb +21 -0
- data/lib/conceptql/nodes/race.rb +23 -0
- data/lib/conceptql/nodes/rxnorm.rb +20 -0
- data/lib/conceptql/nodes/snomed.rb +19 -0
- data/lib/conceptql/nodes/source_vocabulary_node.rb +54 -0
- data/lib/conceptql/nodes/standard_vocabulary_node.rb +43 -0
- data/lib/conceptql/nodes/started_by.rb +16 -0
- data/lib/conceptql/nodes/temporal_node.rb +25 -0
- data/lib/conceptql/nodes/time_window.rb +54 -0
- data/lib/conceptql/nodes/union.rb +15 -0
- data/lib/conceptql/nodes/visit.rb +11 -0
- data/lib/conceptql/nodes/visit_occurrence.rb +26 -0
- data/lib/conceptql/nodifier.rb +9 -0
- data/lib/conceptql/query.rb +39 -0
- data/lib/conceptql/tree.rb +36 -0
- data/lib/conceptql/version.rb +3 -0
- data/lib/conceptql/view_maker.rb +56 -0
- data/lib/conceptql.rb +7 -0
- data/spec/conceptql/behaviors/dottable_spec.rb +111 -0
- data/spec/conceptql/date_adjuster_spec.rb +68 -0
- data/spec/conceptql/nodes/after_spec.rb +18 -0
- data/spec/conceptql/nodes/before_spec.rb +18 -0
- data/spec/conceptql/nodes/casting_node_spec.rb +73 -0
- data/spec/conceptql/nodes/complement_spec.rb +15 -0
- data/spec/conceptql/nodes/concept_spec.rb +34 -0
- data/spec/conceptql/nodes/condition_type_spec.rb +113 -0
- data/spec/conceptql/nodes/cpt_spec.rb +31 -0
- data/spec/conceptql/nodes/date_range_spec.rb +35 -0
- data/spec/conceptql/nodes/death_spec.rb +12 -0
- data/spec/conceptql/nodes/during_spec.rb +32 -0
- data/spec/conceptql/nodes/except_spec.rb +18 -0
- data/spec/conceptql/nodes/first_spec.rb +37 -0
- data/spec/conceptql/nodes/from_spec.rb +15 -0
- data/spec/conceptql/nodes/gender_spec.rb +29 -0
- data/spec/conceptql/nodes/hcpcs_spec.rb +31 -0
- data/spec/conceptql/nodes/icd10_spec.rb +36 -0
- data/spec/conceptql/nodes/icd9_procedure_spec.rb +31 -0
- data/spec/conceptql/nodes/icd9_spec.rb +36 -0
- data/spec/conceptql/nodes/intersect_spec.rb +33 -0
- data/spec/conceptql/nodes/last_spec.rb +38 -0
- data/spec/conceptql/nodes/loinc_spec.rb +31 -0
- data/spec/conceptql/nodes/occurrence_spec.rb +89 -0
- data/spec/conceptql/nodes/person_filter_spec.rb +18 -0
- data/spec/conceptql/nodes/person_spec.rb +12 -0
- data/spec/conceptql/nodes/place_of_service_code_spec.rb +26 -0
- data/spec/conceptql/nodes/procedure_occurrence_spec.rb +12 -0
- data/spec/conceptql/nodes/query_double.rb +19 -0
- data/spec/conceptql/nodes/race_spec.rb +23 -0
- data/spec/conceptql/nodes/rxnorm_spec.rb +31 -0
- data/spec/conceptql/nodes/snomed_spec.rb +31 -0
- data/spec/conceptql/nodes/source_vocabulary_node_spec.rb +37 -0
- data/spec/conceptql/nodes/standard_vocabulary_node_spec.rb +40 -0
- data/spec/conceptql/nodes/started_by_spec.rb +25 -0
- data/spec/conceptql/nodes/temporal_node_spec.rb +57 -0
- data/spec/conceptql/nodes/time_window_spec.rb +66 -0
- data/spec/conceptql/nodes/union_spec.rb +25 -0
- data/spec/conceptql/nodes/visit_occurrence_spec.rb +12 -0
- data/spec/conceptql/query_spec.rb +20 -0
- data/spec/conceptql/tree_spec.rb +54 -0
- data/spec/doubles/stream_for_casting_double.rb +9 -0
- data/spec/doubles/stream_for_occurrence_double.rb +21 -0
- data/spec/doubles/stream_for_temporal_double.rb +6 -0
- data/spec/spec_helper.rb +74 -0
- metadata +327 -0
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'conceptql/nodes/occurrence'
|
3
|
+
require_double('stream_for_occurrence')
|
4
|
+
|
5
|
+
describe ConceptQL::Nodes::Occurrence do
|
6
|
+
it 'behaves itself' do
|
7
|
+
ConceptQL::Nodes::Occurrence.new.must_behave_like(:evaluator)
|
8
|
+
end
|
9
|
+
|
10
|
+
describe 'occurrence set to 1' do
|
11
|
+
subject do
|
12
|
+
ConceptQL::Nodes::Occurrence.new(1, StreamForOccurrenceDouble.new).query(Sequel.mock).sql
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'should order by ascending start_date' do
|
16
|
+
subject.must_match 'ORDER BY start_date ASC'
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'should partition by person_id' do
|
20
|
+
subject.must_match 'PARTITION BY person_id'
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'should assign a row number' do
|
24
|
+
subject.must_match 'row_number()'
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'should find the all rows with rn = 1' do
|
28
|
+
subject.must_match 'rn = 1'
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe 'occurrence set to 2' do
|
33
|
+
subject do
|
34
|
+
ConceptQL::Nodes::Occurrence.new(2, StreamForOccurrenceDouble.new).query(Sequel.mock).sql
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'should order by ascending start_date' do
|
38
|
+
subject.must_match 'ORDER BY start_date ASC'
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'should find the all rows with rn = 2' do
|
42
|
+
subject.must_match 'rn = 2'
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe 'occurrence set to -1' do
|
47
|
+
subject do
|
48
|
+
ConceptQL::Nodes::Occurrence.new(-1, StreamForOccurrenceDouble.new).query(Sequel.mock).sql
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'should order by ascending start_date' do
|
52
|
+
subject.must_match 'ORDER BY start_date DESC'
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'should find the all rows with rn = 1' do
|
56
|
+
subject.must_match 'rn = 1'
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe 'occurrence set to -2' do
|
61
|
+
subject do
|
62
|
+
ConceptQL::Nodes::Occurrence.new(-2, StreamForOccurrenceDouble.new).query(Sequel.mock).sql
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'should order by ascending start_date' do
|
66
|
+
subject.must_match 'ORDER BY start_date DESC'
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'should find the all rows with rn = 2' do
|
70
|
+
subject.must_match 'rn = 2'
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe 'occurrence respects types' do
|
75
|
+
subject do
|
76
|
+
dub = StreamForOccurrenceDouble.new
|
77
|
+
def dub.types
|
78
|
+
[:condition_occurrence]
|
79
|
+
end
|
80
|
+
ConceptQL::Nodes::Occurrence.new(-2, dub).query(Sequel.mock).sql
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'should order by ascending start_date' do
|
84
|
+
subject.must_match ', condition_occurrence_id ASC'
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'conceptql/nodes/person_filter'
|
3
|
+
require_relative 'query_double'
|
4
|
+
|
5
|
+
describe ConceptQL::Nodes::PersonFilter do
|
6
|
+
it 'behaves itself' do
|
7
|
+
ConceptQL::Nodes::PersonFilter.new.must_behave_like(:evaluator)
|
8
|
+
end
|
9
|
+
|
10
|
+
describe '#query' do
|
11
|
+
it 'uses right stream as argument to PERSON_FILTER against left stream' do
|
12
|
+
double1 = QueryDouble.new(1)
|
13
|
+
double2 = QueryDouble.new(2)
|
14
|
+
double1.must_behave_like(:evaluator)
|
15
|
+
ConceptQL::Nodes::PersonFilter.new(left: double1, right: double2).query(Sequel.mock).sql.must_equal "SELECT * FROM (SELECT * FROM table1) AS t1 WHERE (person_id IN (SELECT person_id FROM table2 GROUP BY person_id))"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'conceptql/nodes/place_of_service_code'
|
3
|
+
|
4
|
+
describe ConceptQL::Nodes::PlaceOfServiceCode do
|
5
|
+
it 'behaves itself' do
|
6
|
+
ConceptQL::Nodes::PlaceOfServiceCode.new.must_behave_like(:evaluator)
|
7
|
+
end
|
8
|
+
|
9
|
+
describe '#query' do
|
10
|
+
it 'works for 23' do
|
11
|
+
correct_query = "SELECT * FROM visit_occurrence_with_dates AS v INNER JOIN vocabulary.concept AS vc ON (vc.concept_id = v.place_of_service_concept_id) WHERE (vc.concept_code IN ('23'))"
|
12
|
+
ConceptQL::Nodes::PlaceOfServiceCode.new('23').query(Sequel.mock).sql.must_equal correct_query
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'works for 23 as number' do
|
16
|
+
correct_query = "SELECT * FROM visit_occurrence_with_dates AS v INNER JOIN vocabulary.concept AS vc ON (vc.concept_id = v.place_of_service_concept_id) WHERE (vc.concept_code IN ('23'))"
|
17
|
+
ConceptQL::Nodes::PlaceOfServiceCode.new(23).query(Sequel.mock).sql.must_equal correct_query
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'works for multiple values' do
|
21
|
+
correct_query = "SELECT * FROM visit_occurrence_with_dates AS v INNER JOIN vocabulary.concept AS vc ON (vc.concept_id = v.place_of_service_concept_id) WHERE (vc.concept_code IN ('23', '22'))"
|
22
|
+
ConceptQL::Nodes::PlaceOfServiceCode.new('23', '22').query(Sequel.mock).sql.must_equal correct_query
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'conceptql/nodes/procedure_occurrence'
|
3
|
+
require_double('stream_for_casting')
|
4
|
+
|
5
|
+
describe ConceptQL::Nodes::CastingNode do
|
6
|
+
it 'behaves itself' do
|
7
|
+
ConceptQL::Nodes::ProcedureOccurrence.new.must_behave_like(:casting_node)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
|
12
|
+
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'conceptql/nodes/node'
|
2
|
+
class QueryDouble < ConceptQL::Nodes::Node
|
3
|
+
def initialize(num, type = :visit_occurrence)
|
4
|
+
@num = num
|
5
|
+
@type = type
|
6
|
+
end
|
7
|
+
|
8
|
+
def types
|
9
|
+
[@type]
|
10
|
+
end
|
11
|
+
|
12
|
+
def evaluate(db)
|
13
|
+
query(db)
|
14
|
+
end
|
15
|
+
|
16
|
+
def query(db)
|
17
|
+
db["table#{@num}".to_sym]
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'conceptql/nodes/race'
|
3
|
+
|
4
|
+
describe ConceptQL::Nodes::Race do
|
5
|
+
it 'behaves itself' do
|
6
|
+
ConceptQL::Nodes::Race.new.must_behave_like(:evaluator)
|
7
|
+
end
|
8
|
+
|
9
|
+
describe '#query' do
|
10
|
+
it 'works for white' do
|
11
|
+
correct_query = "SELECT * FROM person_with_dates AS p INNER JOIN vocabulary.concept AS vc ON (vc.concept_id = p.race_concept_id) WHERE (lower(vc.concept_name) IN ('white'))"
|
12
|
+
ConceptQL::Nodes::Race.new('White').query(Sequel.mock).sql.must_equal correct_query
|
13
|
+
ConceptQL::Nodes::Race.new('white').query(Sequel.mock).sql.must_equal correct_query
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'works for multiple values' do
|
17
|
+
correct_query = "SELECT * FROM person_with_dates AS p INNER JOIN vocabulary.concept AS vc ON (vc.concept_id = p.race_concept_id) WHERE (lower(vc.concept_name) IN ('white', 'other'))"
|
18
|
+
ConceptQL::Nodes::Race.new('White', 'Other').query(Sequel.mock).sql.must_equal correct_query
|
19
|
+
ConceptQL::Nodes::Race.new('white', 'other').query(Sequel.mock).sql.must_equal correct_query
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'conceptql/nodes/rxnorm'
|
3
|
+
|
4
|
+
describe ConceptQL::Nodes::Rxnorm do
|
5
|
+
it 'behaves itself' do
|
6
|
+
ConceptQL::Nodes::Rxnorm.new.must_behave_like(:standard_vocabulary_node)
|
7
|
+
end
|
8
|
+
|
9
|
+
subject do
|
10
|
+
ConceptQL::Nodes::Rxnorm.new
|
11
|
+
end
|
12
|
+
|
13
|
+
describe '#table' do
|
14
|
+
it 'should be drug_exposure' do
|
15
|
+
subject.table.must_equal :drug_exposure
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '#concept_column' do
|
20
|
+
it 'should be drug_concept_id' do
|
21
|
+
subject.concept_column.must_equal :drug_concept_id
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe '#vocabulary_id' do
|
26
|
+
it 'should be 8' do
|
27
|
+
subject.vocabulary_id.must_equal 8
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'conceptql/nodes/snomed'
|
3
|
+
|
4
|
+
describe ConceptQL::Nodes::Snomed do
|
5
|
+
it 'behaves itself' do
|
6
|
+
ConceptQL::Nodes::Snomed.new.must_behave_like(:standard_vocabulary_node)
|
7
|
+
end
|
8
|
+
|
9
|
+
subject do
|
10
|
+
ConceptQL::Nodes::Snomed.new
|
11
|
+
end
|
12
|
+
|
13
|
+
describe '#table' do
|
14
|
+
it 'should be condition_occurrence' do
|
15
|
+
subject.table.must_equal :condition_occurrence
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '#concept_column' do
|
20
|
+
it 'should be condition_concept_id' do
|
21
|
+
subject.concept_column.must_equal :condition_concept_id
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe '#vocabulary_id' do
|
26
|
+
it 'should be 1' do
|
27
|
+
subject.vocabulary_id.must_equal 1
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'conceptql/nodes/source_vocabulary_node'
|
3
|
+
|
4
|
+
describe ConceptQL::Nodes::SourceVocabularyNode do
|
5
|
+
it 'behaves itself' do
|
6
|
+
ConceptQL::Nodes::SourceVocabularyNode.new.must_behave_like(:evaluator)
|
7
|
+
end
|
8
|
+
|
9
|
+
class SourceVocabularyDouble < ConceptQL::Nodes::SourceVocabularyNode
|
10
|
+
def table
|
11
|
+
:table
|
12
|
+
end
|
13
|
+
|
14
|
+
def source_column
|
15
|
+
:source_column
|
16
|
+
end
|
17
|
+
|
18
|
+
def concept_column
|
19
|
+
:concept_column
|
20
|
+
end
|
21
|
+
|
22
|
+
def vocabulary_id
|
23
|
+
1
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe '#query' do
|
28
|
+
it 'works for single values' do
|
29
|
+
SourceVocabularyDouble.new('value').query(Sequel.mock).sql.must_equal "SELECT * FROM table_with_dates AS tab INNER JOIN vocabulary.source_to_concept_map AS scm ON (scm.target_concept_id = tab.concept_column) WHERE ((scm.source_code IN ('value')) AND (scm.source_vocabulary_id = 1) AND (scm.source_code = tab.source_column))"
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'works for multiple values' do
|
33
|
+
SourceVocabularyDouble.new('value1', 'value2').query(Sequel.mock).sql.must_equal "SELECT * FROM table_with_dates AS tab INNER JOIN vocabulary.source_to_concept_map AS scm ON (scm.target_concept_id = tab.concept_column) WHERE ((scm.source_code IN ('value1', 'value2')) AND (scm.source_vocabulary_id = 1) AND (scm.source_code = tab.source_column))"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'conceptql/nodes/standard_vocabulary_node'
|
3
|
+
|
4
|
+
describe ConceptQL::Nodes::StandardVocabularyNode do
|
5
|
+
it 'behaves itself' do
|
6
|
+
ConceptQL::Nodes::StandardVocabularyNode.new.must_behave_like(:evaluator)
|
7
|
+
end
|
8
|
+
|
9
|
+
class StandardVocabularyDouble < ConceptQL::Nodes::StandardVocabularyNode
|
10
|
+
def table
|
11
|
+
:table
|
12
|
+
end
|
13
|
+
|
14
|
+
def concept_column
|
15
|
+
:concept_column
|
16
|
+
end
|
17
|
+
|
18
|
+
def vocabulary_id
|
19
|
+
1
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe StandardVocabularyDouble do
|
24
|
+
it 'behaves itself' do
|
25
|
+
StandardVocabularyDouble.new.must_behave_like(:standard_vocabulary_node)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe '#query' do
|
30
|
+
it 'works for single values' do
|
31
|
+
StandardVocabularyDouble.new('value').query(Sequel.mock).sql.must_equal "SELECT * FROM table_with_dates AS tab INNER JOIN vocabulary.concept AS c ON (c.concept_id = tab.concept_column) WHERE ((c.concept_code IN ('value')) AND (c.vocabulary_id = 1))"
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'works for multiple diagnoses' do
|
35
|
+
StandardVocabularyDouble.new('value1', 'value2').query(Sequel.mock).sql.must_equal "SELECT * FROM table_with_dates AS tab INNER JOIN vocabulary.concept AS c ON (c.concept_id = tab.concept_column) WHERE ((c.concept_code IN ('value1', 'value2')) AND (c.vocabulary_id = 1))"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'conceptql/nodes/started_by'
|
3
|
+
require_double('stream_for_temporal')
|
4
|
+
|
5
|
+
describe ConceptQL::Nodes::StartedBy do
|
6
|
+
it 'behaves itself' do
|
7
|
+
ConceptQL::Nodes::StartedBy.new.must_behave_like(:temporal_node)
|
8
|
+
end
|
9
|
+
|
10
|
+
subject do
|
11
|
+
ConceptQL::Nodes::StartedBy.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.start_date'
|
16
|
+
subject.query(Sequel.mock).sql.must_match 'l.end_date > r.end_date'
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'should use proper where clause when inclusive' do
|
20
|
+
sub = ConceptQL::Nodes::StartedBy.new(left: StreamForTemporalDouble.new, right: StreamForTemporalDouble.new, inclusive: true)
|
21
|
+
sub.query(Sequel.mock).sql.must_match 'l.start_date = r.start_date'
|
22
|
+
sub.query(Sequel.mock).sql.must_match 'l.end_date >= r.end_date'
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'conceptql/nodes/temporal_node'
|
3
|
+
require_double('stream_for_temporal')
|
4
|
+
|
5
|
+
describe ConceptQL::Nodes::TemporalNode do
|
6
|
+
it 'behaves itself' do
|
7
|
+
ConceptQL::Nodes::TemporalNode.new.must_behave_like(:evaluator)
|
8
|
+
end
|
9
|
+
|
10
|
+
class TemporalDouble < ConceptQL::Nodes::TemporalNode
|
11
|
+
def where_clause
|
12
|
+
Proc.new do
|
13
|
+
l__end_date < r__start_date
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe TemporalDouble do
|
19
|
+
it 'behaves itself' do
|
20
|
+
TemporalDouble.new.must_behave_like(:temporal_node)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe StreamForTemporalDouble do
|
25
|
+
it 'behaves itself' do
|
26
|
+
StreamForTemporalDouble.new.must_behave_like(:evaluator)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe '#inclusive?' do
|
31
|
+
it 'defaults to false' do
|
32
|
+
refute(TemporalDouble.new.inclusive?)
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'can be set to true' do
|
36
|
+
assert(TemporalDouble.new(inclusive: true).inclusive?)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe '#query' do
|
41
|
+
it 'uses logic from where_clause' do
|
42
|
+
sql = TemporalDouble.new(left: StreamForTemporalDouble.new, right: StreamForTemporalDouble.new).query(Sequel.mock).sql
|
43
|
+
sql.must_match('l.end_date < r.start_date')
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'pulls from the right tables' do
|
47
|
+
sql = TemporalDouble.new(left: StreamForTemporalDouble.new, right: StreamForTemporalDouble.new).query(Sequel.mock).sql
|
48
|
+
sql.must_match('FROM table')
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'is ok with symbols' do
|
52
|
+
sql = TemporalDouble.new(left: StreamForTemporalDouble.new, right: StreamForTemporalDouble.new).query(Sequel.mock).sql
|
53
|
+
sql.must_match('FROM table')
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'conceptql/nodes/time_window'
|
3
|
+
require 'conceptql/nodes/node'
|
4
|
+
|
5
|
+
describe ConceptQL::Nodes::TimeWindow do
|
6
|
+
class Stream4TimeWindowDouble < ConceptQL::Nodes::Node
|
7
|
+
def types
|
8
|
+
[:visit_occurrence]
|
9
|
+
end
|
10
|
+
|
11
|
+
def query(db)
|
12
|
+
db
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe '#evaluate' do
|
17
|
+
it 'adjusts start by 1 day' do
|
18
|
+
sql = ConceptQL::Nodes::TimeWindow.new(Stream4TimeWindowDouble.new, { start: 'd', end: '' }).evaluate(Sequel.mock).sql
|
19
|
+
sql.must_match(%q{date((date(start_date) + interval '1 day')) AS start_date})
|
20
|
+
sql.must_match(%q{date(end_date) AS end_date})
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'adjusts start by 1 day' do
|
24
|
+
sql = ConceptQL::Nodes::TimeWindow.new(Stream4TimeWindowDouble.new, { start: '', end: 'd' }).evaluate(Sequel.mock).sql
|
25
|
+
sql.must_match(%q{date(start_date) AS start_date})
|
26
|
+
sql.must_match(%q{date((date(end_date) + interval '1 day')) AS end_date})
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'adjusts both values by 1 day' do
|
30
|
+
sql = ConceptQL::Nodes::TimeWindow.new(Stream4TimeWindowDouble.new, { start: 'd', end: 'd' }).evaluate(Sequel.mock).sql
|
31
|
+
sql.must_match(%q{date((date(start_date) + interval '1 day')) AS start_date, date((date(end_date) + interval '1 day')) AS end_date})
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'makes multiple adjustments to both values' do
|
35
|
+
sql = ConceptQL::Nodes::TimeWindow.new(Stream4TimeWindowDouble.new, { start: 'dmy', end: '-d-m-y' }).evaluate(Sequel.mock).sql
|
36
|
+
sql.must_match(%q{date((date((date((date(start_date) + interval '1 day')) + interval '1 month')) + interval '1 year'))})
|
37
|
+
sql.must_match(%q{date((date((date((date(end_date) + interval '-1 day')) + interval '-1 month')) + interval '-1 year'))})
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'can set start_date to be end_date' do
|
41
|
+
sql = ConceptQL::Nodes::TimeWindow.new(Stream4TimeWindowDouble.new, { start: 'end', end: '' }).evaluate(Sequel.mock).sql
|
42
|
+
sql.must_match(%q{end_date AS start_date})
|
43
|
+
sql.must_match(%q{date(end_date) AS end_date})
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'can set end_date to be start_date' do
|
47
|
+
sql = ConceptQL::Nodes::TimeWindow.new(Stream4TimeWindowDouble.new, { start: '', end: 'start' }).evaluate(Sequel.mock).sql
|
48
|
+
sql.must_match(%q{start_date AS end_date})
|
49
|
+
sql.must_match(%q{date(start_date) AS start_date})
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'will swap start and end dates, though this is a bad idea but you should probably know about this' do
|
53
|
+
sql = ConceptQL::Nodes::TimeWindow.new(Stream4TimeWindowDouble.new, { start: 'end', end: 'start' }).evaluate(Sequel.mock).sql
|
54
|
+
sql.must_match(%q{start_date AS end_date})
|
55
|
+
sql.must_match(%q{end_date AS start_date})
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'handles nil arguments to both start and end' do
|
59
|
+
sql = ConceptQL::Nodes::TimeWindow.new(Stream4TimeWindowDouble.new, { start: nil, end: nil }).evaluate(Sequel.mock).sql
|
60
|
+
sql.must_match(%q{date(start_date) AS start_date})
|
61
|
+
sql.must_match(%q{date(end_date) AS end_date})
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'conceptql/nodes/union'
|
3
|
+
require_relative 'query_double'
|
4
|
+
|
5
|
+
describe ConceptQL::Nodes::Union do
|
6
|
+
it 'behaves itself' do
|
7
|
+
ConceptQL::Nodes::Union.new.must_behave_like(:evaluator)
|
8
|
+
end
|
9
|
+
|
10
|
+
describe '#query' do
|
11
|
+
it 'works for multiple criteria' do
|
12
|
+
double1 = QueryDouble.new(1)
|
13
|
+
double2 = QueryDouble.new(2)
|
14
|
+
double3 = QueryDouble.new(3)
|
15
|
+
double1.must_behave_like(:evaluator)
|
16
|
+
ConceptQL::Nodes::Union.new(double1, double2, double3).query(Sequel.mock).sql.must_equal "SELECT * FROM (SELECT * FROM (SELECT * FROM table1 UNION ALL SELECT * FROM table2) AS t1 UNION ALL SELECT * FROM table3) AS t1"
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'works for single criteria' do
|
20
|
+
double1 = QueryDouble.new(1)
|
21
|
+
double1.must_behave_like(:evaluator)
|
22
|
+
ConceptQL::Nodes::Union.new(double1).query(Sequel.mock).sql.must_equal "SELECT * FROM table1"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'conceptql/nodes/visit_occurrence'
|
3
|
+
require_double('stream_for_casting')
|
4
|
+
|
5
|
+
describe ConceptQL::Nodes::CastingNode do
|
6
|
+
it 'behaves itself' do
|
7
|
+
ConceptQL::Nodes::VisitOccurrence.new.must_behave_like(:casting_node)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
|
12
|
+
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'conceptql'
|
3
|
+
|
4
|
+
describe ConceptQL::Query do
|
5
|
+
describe '#query' do
|
6
|
+
it 'passes request on to tree' do
|
7
|
+
yaml = Psych.dump({ icd9: '799.22' })
|
8
|
+
mock_tree = Minitest::Mock.new
|
9
|
+
mock_node = Minitest::Mock.new
|
10
|
+
|
11
|
+
query = ConceptQL::Query.new(:mock_db, yaml, mock_tree)
|
12
|
+
mock_tree.expect :root, mock_node, [query]
|
13
|
+
mock_node.expect :evaluate, nil, [:mock_db]
|
14
|
+
query.query
|
15
|
+
|
16
|
+
mock_node.verify
|
17
|
+
mock_tree.verify
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'conceptql/tree'
|
3
|
+
|
4
|
+
describe ConceptQL::Tree do
|
5
|
+
describe '#root' do
|
6
|
+
before do
|
7
|
+
@mock_query_obj = Minitest::Mock.new
|
8
|
+
@mock_nodifier = Minitest::Mock.new
|
9
|
+
@tree = ConceptQL::Tree.new(nodifier: @mock_nodifier)
|
10
|
+
end
|
11
|
+
|
12
|
+
after do
|
13
|
+
@mock_query_obj.verify
|
14
|
+
@mock_nodifier.verify
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'should walk single node criteria tree and convert to node' do
|
18
|
+
@mock_nodifier.expect :create, :success_indicator, [:icd9, '799.22']
|
19
|
+
@mock_query_obj.expect :statement, { icd9: '799.22' }
|
20
|
+
|
21
|
+
@tree.root(@mock_query_obj).must_equal :success_indicator
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'should extend all nodes created with behavior passed in' do
|
25
|
+
mock_icd9_obj = Minitest::Mock.new
|
26
|
+
mock_icd9_obj.expect :extend, nil, [:mock_behavior]
|
27
|
+
|
28
|
+
@mock_nodifier.expect :create, mock_icd9_obj, [:icd9, '799.22']
|
29
|
+
@mock_query_obj.expect :statement, { icd9: '799.22' }
|
30
|
+
|
31
|
+
tree = ConceptQL::Tree.new(nodifier: @mock_nodifier, behavior: :mock_behavior)
|
32
|
+
tree.root(@mock_query_obj)
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'should walk multi-criteria node' do
|
36
|
+
@mock_nodifier.expect :create, :mock_icd9, [:icd9, '799.22']
|
37
|
+
@mock_nodifier.expect :create, :success_indicator, [:nth, { occurrence: 1, expression: :mock_icd9 }]
|
38
|
+
|
39
|
+
@mock_query_obj.expect :statement, { nth: { occurrence: 1, expression: { icd9: '799.22' } } }
|
40
|
+
|
41
|
+
@tree.root(@mock_query_obj).must_equal :success_indicator
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'should walk multi-node criteria tree and convert to nodes' do
|
45
|
+
@mock_nodifier.expect :create, :mock_icd9, [:icd9, '799.22']
|
46
|
+
@mock_nodifier.expect :create, :success_indicator, [:any, [:mock_icd9]]
|
47
|
+
|
48
|
+
@mock_query_obj.expect :statement, { any: [{ icd9: '799.22' }] }
|
49
|
+
|
50
|
+
@tree.root(@mock_query_obj).must_equal :success_indicator
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class StreamForOccurrenceDouble < ConceptQL::Nodes::Node
|
2
|
+
def query(db)
|
3
|
+
ds = db.from(:table)
|
4
|
+
# Occurrence needs window functions to work
|
5
|
+
meta_def(ds, :supports_window_functions?){true}
|
6
|
+
ds
|
7
|
+
end
|
8
|
+
|
9
|
+
# Stole this from:
|
10
|
+
# https://github.com/jeremyevans/sequel/blob/63397b787335d06de97dc89ddf49b7a3a93ffdc9/spec/core/expression_filters_spec.rb#L400
|
11
|
+
#
|
12
|
+
# By default, the Sequel.mock datasets don't allow window functions, but I need them
|
13
|
+
# enabled for testing
|
14
|
+
#
|
15
|
+
# I saw that Sequel tests had this little nugget in them to temporarily enable
|
16
|
+
# window functions and sure enough, it works
|
17
|
+
def meta_def(obj, name, &block)
|
18
|
+
(class << obj; self end).send(:define_method, name, &block)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|