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,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,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,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,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,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
|