conceptql 0.0.3 → 0.0.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 09e9c3738772ff72625b848a26ac0e6d98c8275a
4
- data.tar.gz: eb7753304d9a505e29c805019c222fe3d7a3119c
3
+ metadata.gz: 9e181385920abe60a6094cfddfac2e8991688084
4
+ data.tar.gz: 0394dd07282ac56252927290a597749a68798b60
5
5
  SHA512:
6
- metadata.gz: e761811c0bcbcb2745cc5f3c9a0131d83a1b51ad26de632c3c02ca4d0f3b84ce6ba468f454a66a940027289d027747304501f52ecee648f41aa8f57aa91d98d5
7
- data.tar.gz: 65b907da6c8bddb46d61a772dfc4c965fa1c8c87e3d1729b5ec0175c047b9860043b5760eba9583ce3c2f12d18cfc9dc901114d79a7cae410282e9c6e9dbefd2
6
+ metadata.gz: c65a2c6339d95b2ac59f75eebd81c6f38df7a219c6f51c17849d12799263a5a488f748f144b992f7c6069bc7fa0f49f266a20f106d54f58b36e2c160981b8bdc
7
+ data.tar.gz: 4cad8f6903fc03e7780f3428520ecd4a74befd30cc90e853febc6808ca6ab9b5a472471f005317f587832ddbd6a655607222ba3a405515492815c49eaa2a5be5
data/CHANGELOG.md CHANGED
@@ -1,6 +1,24 @@
1
1
  # Changelog
2
2
  All notable changes to this project will be documented in this file.
3
3
 
4
+ ## 0.0.4 - 2014-08-19
5
+
6
+ ### Added
7
+ - Support for 5 instead of 13 column internal representation of results.
8
+ - `define` node, used to create "variables" in ConceptQL.
9
+ - `recall` node, used to pull results from "variables" in ConceptQL.
10
+
11
+ ### Deprecated
12
+ - Nothing.
13
+
14
+ ### Removed
15
+ - Support for 13 column results.
16
+ - Dependency on a set of views to run SQL queries.
17
+
18
+ ### Fixed
19
+ - Bug where `place_of_service_code` wasn't limited to vocabulary_id 14
20
+
21
+
4
22
  ## 0.0.3 - 2014-08-12
5
23
 
6
24
  ### Added
data/README.md CHANGED
@@ -45,6 +45,11 @@ As stated above, one of the goals of ConcegtQL is to make it easy to assemble fa
45
45
  If you're interested in reading more about ConceptQL, a rought draft of the specifications document is [available as a PDF](https://github.com/outcomesinsights/conceptql/blob/master/doc/ConceptQL%20Specification%20(alpha).pdf?raw=true) in this repository.
46
46
 
47
47
 
48
+ ## Try Before You Buy
49
+
50
+ If you'd like to interact with ConceptQL a bit before deciding to dive in, head over to the [ConceptQL Sandbox](http://sandbox.cohortjigsaw.com) for an online demonstration of the language and its features.
51
+
52
+
48
53
  ## Requirements
49
54
 
50
55
  ConceptQL is in an early-alpha state. For now it is limited to working with [OMOP CDM](http://omop.org/CDM)-structured data stored in the [PostgreSQL](http://www.postgresql.org/) database. It has been tested under [Ubuntu Linux](http://www.ubuntu.com/) and Mac OS X 10.8+. The interpreter is written in Ruby and theoretically should be platform independent, but your mileage may vary.
@@ -75,7 +80,7 @@ Or install it yourself as:
75
80
 
76
81
  ## Usage
77
82
 
78
- The easiest way to try out ConceptQL is to use the [conceptql-dev-box](http://github.com/outcomesinsights/conceptql-dev-box)
83
+ The easiest way to try out ConceptQL locally is to use the [conceptql-dev-box](http://github.com/outcomesinsights/conceptql-dev-box)
79
84
 
80
85
  ConceptQL comes with a [Thor](http://whatisthor.com/)-based command-line program: `conceptql`
81
86
 
data/doc/spec.md CHANGED
@@ -1049,6 +1049,47 @@ I don't have a good feel for:
1049
1049
  - Do we throw an exception if not?
1050
1050
  - Do we require calling programs to invoke a check on the concept before generating the query?
1051
1051
 
1052
+
1053
+ ##### Update 2014-08-13
1054
+ I've hacked some variable support into an experimental branch of ConceptQL. So far, here's how it works:
1055
+ - Define node
1056
+ - Give it two params, a name, and a stream
1057
+ - Stream is used to populate a temporary table that is named after the name
1058
+ - name is plain english sentence that is then turned into a hexdigest for a name
1059
+ - Avoids collisions with other names
1060
+ - Avoids truncation issues if name is WAY long
1061
+ - Recall node
1062
+ - Takes a single argument: the name used in a define node
1063
+ - Re-written to fetch results from the temp table that has the name provided
1064
+
1065
+ Current issues:
1066
+
1067
+ - Sequel's create table statement runs out-of-band with rest of ConceptQL statemnt
1068
+ - Gets executed immediately
1069
+ - Type information in "define" needs to be made available to "from"
1070
+ - Currently attempting to pass this information from define to from using an attribute tacked onto the shared db connection
1071
+ - Recall may not have access to this information until #query is called
1072
+ - This is bad and needs to be fixed/rethought
1073
+
1074
+
1075
+ Considerations for the future:
1076
+ - Probably want to rename these nodes to something better
1077
+ - It would still be nice to drop a concept into a concept that has "slots" waiting
1078
+ - Perhaps slot is a different node from "define"
1079
+ - I had to retool Tree and Query and Graph to expect an array of concepts in a ConceptQL statement
1080
+ - I'm not sure I like this
1081
+ - A ConceptQL statement perhaps should be only a single statement at the end
1082
+ - If an array of sub-concepts is fed into Query, maybe we only execute the last one after parsing the others
1083
+ - This is consistent with how Sequel wants to live and would yield a single set of results
1084
+ - I think I like this
1085
+ - Defines need to occur before they are used
1086
+ - Most languages have a "forward definition" ability
1087
+ - I have no use cases for when we might need those?
1088
+ - Perhaps a definition that uses a definition that doesn't exist?
1089
+ - Is that recursive?
1090
+ - Is that something we want/need in ConceptQL?
1091
+
1092
+
1052
1093
  ### Value Nodes
1053
1094
  So far, we can’t recreate the Charlson comorbidity index using ConceptQL. If we added a “value” node, we could.
1054
1095
 
@@ -44,6 +44,7 @@ module ConceptQL
44
44
  me = g.add_nodes(node_name)
45
45
  me[:label] = display_name
46
46
  me[:color] = type_color(types)
47
+ me[:shape] = shape if respond_to?(:shape)
47
48
  me
48
49
  end
49
50
  end
data/lib/conceptql/cli.rb CHANGED
@@ -49,7 +49,7 @@ module ConceptQL
49
49
 
50
50
  desc 'show_and_tell_file statement_file', 'Reads the ConceptQL statement from the file and shows the contents as a ConceptQL graph, then executes the statement against the DB'
51
51
  option :full
52
- def show_and_tell(file)
52
+ def show_and_tell_file(file)
53
53
  show_and_tell(criteria_from_file(file), options)
54
54
  end
55
55
 
@@ -34,14 +34,16 @@ module ConceptQL
34
34
  attr :yaml, :tree, :db
35
35
 
36
36
  def build_graph(g)
37
- last_node = tree.root(self)
38
- last_node.graph_it(g, db)
39
- if dangler
40
- blank_node = g.add_nodes('')
41
- blank_node[:shape] = 'none'
42
- blank_node[:height] = 0
43
- blank_node[:fixedsize] = true
44
- last_node.link_to(g, blank_node)
37
+ tree.root(self).each.with_index do |last_node, index|
38
+ last_node.graph_it(g, db)
39
+ if dangler
40
+ blank_node = g.add_nodes("_#{index}")
41
+ blank_node[:shape] = 'none'
42
+ blank_node[:height] = 0
43
+ blank_node[:label] = ''
44
+ blank_node[:fixedsize] = true
45
+ last_node.link_to(g, blank_node)
46
+ end
45
47
  end
46
48
  end
47
49
  end
@@ -11,6 +11,7 @@ module ConceptQL
11
11
  condition: :condition_occurrence,
12
12
  primary_diagnosis: :condition_occurrence,
13
13
  icd9: :condition_occurrence,
14
+ icd10: :condition_occurrence,
14
15
  condition_type: :condition_occurrence,
15
16
 
16
17
  # Procedures
@@ -24,6 +25,7 @@ module ConceptQL
24
25
  # Visits
25
26
  visit_occurrence: :visit_occurrence,
26
27
  place_of_service: :visit_occurrence,
28
+ place_of_service_code: :visit_occurrence,
27
29
 
28
30
  # Person
29
31
  person: :person,
@@ -41,7 +43,10 @@ module ConceptQL
41
43
 
42
44
  # Drug
43
45
  drug_exposure: :drug_exposure,
46
+ rxnorm: :drug_exposure,
44
47
  drug_cost: :drug_cost,
48
+ drug_type_concept_id: :drug_exposure,
49
+ drug_type_concept: :drug_exposure,
45
50
 
46
51
  # Date Nodes
47
52
  date_range: :date,
@@ -120,11 +125,62 @@ module ConceptQL
120
125
  end
121
126
  end
122
127
 
128
+ class DefineNode < DotNode
129
+ def initialize(*args)
130
+ @gn = args.pop
131
+ super(*args)
132
+ end
133
+
134
+ def types
135
+ @gn.types[namify(arguments.first)] = super
136
+ end
137
+
138
+ def shape
139
+ :cds
140
+ end
141
+ end
142
+
143
+ class FromNode < DotNode
144
+ def initialize(*args)
145
+ @gn = args.pop
146
+ super(*args)
147
+ end
148
+
149
+ def types
150
+ @gn.types[namify(arguments.first)]
151
+ end
152
+
153
+ def shape
154
+ :cds
155
+ end
156
+ end
157
+
158
+ class VsacNode < DotNode
159
+ def initialize(name, values, types)
160
+ @types = types
161
+ super(name, values)
162
+ end
163
+
164
+ def types
165
+ [ @types ].flatten.compact.map(&:to_sym)
166
+ end
167
+ end
168
+
123
169
  BINARY_OPERATOR_TYPES = %w(before after meets met_by started_by starts contains during overlaps overlapped_by finished_by finishes coincides except person_filter less_than less_than_or_equal equal not_equal greater_than greater_than_or_equal filter).map { |temp| [temp, "not_#{temp}"] }.flatten.map(&:to_sym)
124
170
 
171
+ def types
172
+ @types ||= {}
173
+ end
125
174
  def create(type, values)
126
175
  if BINARY_OPERATOR_TYPES.include?(type)
127
176
  return BinaryOperatorNode.new(type, values)
177
+ elsif type == :define
178
+ return DefineNode.new(type, values, self)
179
+ elsif type == :from
180
+ return FromNode.new(type, values, self)
181
+ elsif type == :vsac
182
+ types = values.pop
183
+ return VsacNode.new(type, values, types)
128
184
  end
129
185
  DotNode.new(type, values)
130
186
  end
@@ -24,7 +24,11 @@ module ConceptQL
24
24
  # rows in its table as results.
25
25
  class CastingNode < Node
26
26
  def types
27
- [my_type]
27
+ [type]
28
+ end
29
+
30
+ def type
31
+ my_type
28
32
  end
29
33
 
30
34
  def castables
@@ -39,36 +43,52 @@ module ConceptQL
39
43
  private
40
44
 
41
45
  def base_query(db, stream_query)
42
- if (stream.types & castables).length < stream.types.length
43
- # We have a situation where one of the incoming streams
46
+ uncastable_types = stream.types - castables
47
+ to_me_types = stream.types & these_point_at_me
48
+ from_me_types = stream.types & i_point_at
49
+
50
+ destination_table = make_table_name(my_type)
51
+ casting_query = db.from(destination_table)
52
+ wheres = []
53
+
54
+ unless uncastable_types.empty?
55
+ # We have a situation where one or more of the incoming streams
44
56
  # isn't castable so we'll just return all rows for
45
- # all people
46
- db.from(make_table_name(my_type))
47
- .where(person_id: db.from(stream_query).select_group(:person_id))
48
- else
49
- # Every type in the stream is castable, so let's setup a query that
57
+ # all people in each uncastable stream
58
+ uncastable_person_ids = db.from(stream_query)
59
+ .where(criterion_type: uncastable_types.map(&:to_s))
60
+ .select_group(:person_id)
61
+ wheres << Sequel.expr(person_id: uncastable_person_ids)
62
+ end
63
+
64
+ destination_type_id = type_id(my_type)
65
+
66
+ unless to_me_types.empty?
67
+ # For each castable type in the stream, setup a query that
50
68
  # casts each type to a set of IDs, union those IDs and fetch
51
69
  # them from the source table
52
- my_ids = stream.types.map do |type|
53
- cast_type(db, type, stream_query)
54
- end.inject do |union_query, query|
55
- union_query.union(query)
56
- end
70
+ castable_type_queries = to_me_types.map do |source_type|
71
+ source_ids = db.from(stream_query)
72
+ .where(criterion_type: source_type.to_s)
73
+ .select_group(:criterion_id)
74
+ source_table = make_table_name(source_type)
75
+ source_type_id = type_id(source_type)
57
76
 
58
- db.from(make_table_name(my_type))
59
- .where(type_id(my_type) => my_ids)
77
+ db.from(source_table)
78
+ .where(source_type_id => source_ids)
79
+ .select(destination_type_id)
80
+ end
81
+ wheres << Sequel.expr(destination_type_id => castable_type_queries)
60
82
  end
61
- end
62
83
 
63
- def cast_type(db, type, stream_query)
64
- query = if i_point_at.include?(type)
65
- db.from(make_table_name(my_type))
66
- .where(type_id(type) => db.from(stream_query.select_group(type_id(type))))
67
- else
68
- db.from(make_table_name(type))
69
- .where(type_id(type) => db.from(stream_query.select_group(type_id(type))))
84
+ unless from_me_types.empty?
85
+ from_me_types.each do |from_me_type|
86
+ fk_type_id = type_id(from_me_type)
87
+ wheres << Sequel.expr(fk_type_id => db.from(stream_query).where(criterion_type: from_me_type.to_s).select_group(:criterion_id))
88
+ end
70
89
  end
71
- query.select(type_id(my_type))
90
+
91
+ casting_query.where(wheres.inject(&:|))
72
92
  end
73
93
  end
74
94
  end
@@ -6,11 +6,30 @@ module ConceptQL
6
6
  def query(db)
7
7
  child = children.first
8
8
  child.types.map do |type|
9
- select_it(db.from(make_table_name(type)).exclude(type_id(type) => child.evaluate(db).select(type_id(type)).from_self.exclude(type_id(type) => nil)), [type])
9
+ positive_query = db.from(child.evaluate(db))
10
+ .select(:criterion_id)
11
+ .exclude(:criterion_id => nil)
12
+ .where(:criterion_type => type.to_s)
13
+ query = db.from(make_table_name(type))
14
+ .exclude(type_id(type) => positive_query)
15
+ db.from(select_it(query, type))
10
16
  end.inject do |union_query, q|
11
17
  union_query.union(q, all: true)
12
18
  end
13
19
  end
20
+
21
+ =begin
22
+ This is an alternate, but equally accurate way to do complement.
23
+ We'll need to benchmark which is faster.
24
+ def query2(db)
25
+ child = children.first
26
+ froms = child.types.map do |type|
27
+ select_it(db.from(make_table_name(type)), type)
28
+ end
29
+ big_from = froms.inject { |union_query, q| union_query.union(q, all:true) }
30
+ db.from(big_from).except(child.evaluate(db))
31
+ end
32
+ =end
14
33
  end
15
34
  end
16
35
  end
@@ -10,12 +10,12 @@ module ConceptQL
10
10
  #
11
11
  # Multiple types can be specified at once
12
12
  class ConditionType < Node
13
- def types
14
- [:condition_occurrence]
13
+ def type
14
+ :condition_occurrence
15
15
  end
16
16
 
17
17
  def query(db)
18
- db.from(:condition_occurrence_with_dates)
18
+ db.from(:condition_occurrence)
19
19
  .where(condition_type_concept_id: condition_occurrence_type_concept_ids)
20
20
  end
21
21
 
@@ -10,6 +10,8 @@ module ConceptQL
10
10
  class DateRange < Node
11
11
  def query(db)
12
12
  db.from(:person)
13
+ .select_append(Sequel.cast('person', :text).as(:criterion_type))
14
+ .select_append(Sequel.expr(:person_id).as(:criterion_id))
13
15
  .select_append(Sequel.expr(start_date(db)).cast(:date).as(:start_date),Sequel.expr(end_date(db)).cast(:date).as(:end_date)).from_self
14
16
  end
15
17
 
@@ -0,0 +1,75 @@
1
+ require_relative 'node'
2
+
3
+ module ConceptQL
4
+ module Nodes
5
+ # Mimics creating a variable name that stores an itermediate result as
6
+ # part of a larger concept
7
+ #
8
+ # The idea is that a concept might be very complex and it helps to break
9
+ # that complex concept into a set of sub-concepts to better understand it.
10
+ #
11
+ # Also, sometimes a particular piece of a concept is used in many places,
12
+ # so it makes sense to write that piece out once, store it as a "variable"
13
+ # and then insert that variable into the concept as needed.
14
+ # run the query once and subsequent calls
15
+ class Define < Node
16
+ # Create a temporary table and store the stream of results in that table.
17
+ # This "caches" the results so we only have to execute stream's query
18
+ # once.
19
+ #
20
+ # The logic here is that if something is assigned to a variable, chances
21
+ # are that it will be used more than once, so why run the query more than
22
+ # once?
23
+ #
24
+ # ConceptQL's SQL generator normally translates the entire statement into
25
+ # one, large query that can be executed later.
26
+ #
27
+ # Unfortunately, Sequel's "create_table" function actually executes the
28
+ # 'CREATE TABLE' SQL right away, meaning that the "define" node will
29
+ # execute immediately _during_ the processing of the ConceptQL statement.
30
+ # We'll see what kinds of problems this causes
31
+ #
32
+ # Lastly, this node does NOT pass its results to the next node. The
33
+ # reason for this exception is to allow us to return the SQL that
34
+ # generates the temp table. This is done so that the ConceptQL sandbox
35
+ # can return the entire set of SQL statements needed to run a query.
36
+ #
37
+ # Perhaps in the future we can find a way around this.
38
+ #
39
+ # Also, things will blow up if you try to use a variable that hasn't been
40
+ # defined yet.
41
+ def query(db)
42
+ table_name = namify(arguments.first)
43
+ stash_types(db, table_name, types)
44
+ db.create_table!(table_name, temp: true, as: stream.evaluate(db))
45
+ db[db.send(:create_table_as_sql, table_name, stream.evaluate(db).sql, temp: true)]
46
+ end
47
+
48
+ def types
49
+ stream.types
50
+ end
51
+
52
+ private
53
+
54
+ # TODO: Fix this. This is shameful.
55
+ # In order to remember which types a table is storing, we need to preserve
56
+ # the type information outside of the "define" statement because the "recall"
57
+ # statement most likely doesn't have a reference to this node (that's the
58
+ # whole point).
59
+ #
60
+ # There is only one object shared between all the nodes: the database
61
+ # connection. We'll do something terrible and piggyback the type
62
+ # information about this variable on the database connection so that
63
+ # the "recall" node can pull that information back out.
64
+ #
65
+ # Ugh.
66
+ def stash_types(db, name, types)
67
+ def db.types
68
+ @types ||= {}
69
+ end
70
+ db.types[name] = types
71
+ end
72
+ end
73
+ end
74
+ end
75
+
@@ -0,0 +1,18 @@
1
+ require_relative 'node'
2
+
3
+ module ConceptQL
4
+ module Nodes
5
+ class DrugTypeConcept < Node
6
+ def type
7
+ :drug_exposure
8
+ end
9
+
10
+ def query(db)
11
+ db.from(:drug_exposure)
12
+ .where(drug_type_concept_id: arguments)
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+
@@ -3,8 +3,8 @@ require_relative 'node'
3
3
  module ConceptQL
4
4
  module Nodes
5
5
  class Gender < Node
6
- def types
7
- [:person]
6
+ def type
7
+ :person
8
8
  end
9
9
 
10
10
  def query(db)
@@ -19,7 +19,7 @@ module ConceptQL
19
19
  end
20
20
  end
21
21
 
22
- db.from(:person_with_dates)
22
+ db.from(:person)
23
23
  .where(gender_concept_id: gender_concept_ids)
24
24
  end
25
25
  end
@@ -10,8 +10,9 @@ module ConceptQL
10
10
  def query(db)
11
11
  exprs = {}
12
12
  values.each do |expression|
13
+ evaled = expression.evaluate(db)
13
14
  expression.types.each do |type|
14
- (exprs[type] ||= []) << expression.evaluate(db)
15
+ (exprs[type] ||= []) << evaled
15
16
  end
16
17
  end
17
18
  typed_queries = exprs.map do |type, queries|
@@ -2,18 +2,6 @@ require 'active_support/core_ext/hash'
2
2
  module ConceptQL
3
3
  module Nodes
4
4
  class Node
5
- KNOWN_TYPES = %i(
6
- condition_occurrence
7
- death
8
- drug_cost
9
- drug_exposure
10
- observation
11
- payer_plan_period
12
- procedure_cost
13
- procedure_occurrence
14
- visit_occurrence
15
- )
16
-
17
5
  attr :values, :options
18
6
  def initialize(*args)
19
7
  args.flatten!
@@ -28,12 +16,13 @@ module ConceptQL
28
16
  select_it(query(db))
29
17
  end
30
18
 
31
- def select_it(query, select_types=types)
32
- query.select(*columns(select_types))
19
+ def select_it(query, specific_type = nil)
20
+ specific_type = type if specific_type.nil? && respond_to?(:type)
21
+ query.select(*columns(specific_type))
33
22
  end
34
23
 
35
24
  def types
36
- @types ||= children.map(&:types).flatten.uniq
25
+ @types ||= determine_types
37
26
  end
38
27
 
39
28
  def children
@@ -48,23 +37,98 @@ module ConceptQL
48
37
  @arguments ||= values.reject { |v| v.is_a?(Node) }
49
38
  end
50
39
 
51
- def columns(select_types = types)
52
- [:person_id___person_id] + KNOWN_TYPES.map do |known_type|
53
- select_types.include?(known_type) ? "#{known_type}_id___#{known_type}_id".to_sym : Sequel.expr(nil).cast(:bigint).as("#{known_type}_id".to_sym)
54
- end + date_columns
40
+ def columns(local_type = nil)
41
+ criterion_type = Sequel.expr(:criterion_type)
42
+ if local_type
43
+ criterion_type = Sequel.expr(local_type.to_s).cast(:text)
44
+ end
45
+ [:person_id___person_id,
46
+ Sequel.expr(type_id(local_type)).as(:criterion_id),
47
+ criterion_type.as(:criterion_type)] + date_columns(local_type)
55
48
  end
56
49
 
57
- def date_columns
58
- [:start_date, :end_date]
50
+ private
51
+ def criterion_id
52
+ :criterion_id
59
53
  end
60
54
 
61
- private
62
- def type_id(type)
55
+ def type_id(type = nil)
56
+ return :criterion_id if type.nil?
57
+ type = :person if type == :death
63
58
  (type.to_s + '_id').to_sym
64
59
  end
65
60
 
66
61
  def make_table_name(table)
67
- "#{table}_with_dates___tab".to_sym
62
+ "#{table}___tab".to_sym
63
+ end
64
+
65
+ def date_columns(type = nil)
66
+ return [:start_date, :end_date] unless type
67
+ sd = start_date_column(type)
68
+ sd = Sequel.expr(sd).cast(:date).as(:start_date) unless sd == :start_date
69
+ ed = end_date_column(type)
70
+ ed = Sequel.expr(ed).cast(:date).as(:end_date) unless ed == :end_date
71
+ [sd, ed]
72
+ end
73
+
74
+ def start_date_column(type)
75
+ {
76
+ condition_occurrence: :condition_start_date,
77
+ death: :death_date,
78
+ drug_exposure: :drug_exposure_start_date,
79
+ drug_cost: nil,
80
+ payer_plan_period: :payer_plan_period_start_date,
81
+ person: person_date_of_birth,
82
+ procedure_occurrence: :procedure_date,
83
+ procedure_cost: nil,
84
+ observation: :observation_date,
85
+ visit_occurrence: :visit_start_date
86
+ }[type]
87
+ end
88
+
89
+ def end_date_column(type)
90
+ {
91
+ condition_occurrence: :condition_end_date,
92
+ death: :death_date,
93
+ drug_exposure: :drug_exposure_end_date,
94
+ drug_cost: nil,
95
+ payer_plan_period: :payer_plan_period_end_date,
96
+ person: person_date_of_birth,
97
+ procedure_occurrence: :procedure_date,
98
+ procedure_cost: nil,
99
+ observation: :observation_date,
100
+ visit_occurrence: :visit_end_date
101
+ }[type]
102
+ end
103
+
104
+ def person_date_of_birth
105
+ assemble_date(:year_of_birth, :month_of_birth, :day_of_birth)
106
+ end
107
+
108
+ def assemble_date(*symbols)
109
+ strings = symbols.map do |symbol|
110
+ Sequel.function(:coalesce, Sequel.expr(symbol).cast(:text), Sequel.expr('01').cast(:text)).cast(:text)
111
+ end
112
+ strings = strings.zip(['-'] * (symbols.length - 1)).flatten.compact
113
+ Sequel.function(:date, Sequel.join(strings))
114
+ end
115
+
116
+ def determine_types
117
+ if children.empty?
118
+ if respond_to?(:type)
119
+ [type]
120
+ else
121
+ raise "Node doesn't seem to specify any type"
122
+ end
123
+ else
124
+ children.map(&:types).flatten.uniq
125
+ end
126
+ end
127
+
128
+ def namify(name)
129
+ require 'digest'
130
+ digest = Digest::SHA256.hexdigest name
131
+ ('_' + digest).to_sym
68
132
  end
69
133
  end
70
134
  end
@@ -23,7 +23,7 @@ module ConceptQL
23
23
  # occurrence, this node returns nothing for that person
24
24
  class Occurrence < Node
25
25
  def query(db)
26
- db.from(stream.evaluate(db)
26
+ db.from(db.from(stream.evaluate(db))
27
27
  .select_append { |o| o.row_number(:over, partition: :person_id, order: ordered_columns){}.as(:rn) })
28
28
  .where(rn: occurrence.abs)
29
29
  end
@@ -39,7 +39,7 @@ module ConceptQL
39
39
 
40
40
  def ordered_columns
41
41
  ordered_columns = [Sequel.send(asc_or_desc, :start_date)]
42
- ordered_columns += types.map { |t| Sequel.asc(type_id(t)) }
42
+ ordered_columns += [:criterion_type, :criterion_id]
43
43
  end
44
44
  end
45
45
  end
@@ -9,14 +9,15 @@ module ConceptQL
9
9
  # concept_name column of the concept table. If you misspell the place_of_service_code name
10
10
  # you won't get any matches
11
11
  class PlaceOfServiceCode < Node
12
- def types
13
- [:visit_occurrence]
12
+ def type
13
+ :visit_occurrence
14
14
  end
15
15
 
16
16
  def query(db)
17
- db.from(:visit_occurrence_with_dates___v)
17
+ db.from(:visit_occurrence___v)
18
18
  .join(:vocabulary__concept___vc, { vc__concept_id: :v__place_of_service_concept_id })
19
19
  .where(vc__concept_code: arguments.map(&:to_s))
20
+ .where(vc__vocabulary_id: 14)
20
21
  end
21
22
  end
22
23
  end
@@ -9,12 +9,12 @@ module ConceptQL
9
9
  # concept_name column of the concept table. If you misspell the race name
10
10
  # you won't get any matches
11
11
  class Race < Node
12
- def types
13
- [:person]
12
+ def type
13
+ :person
14
14
  end
15
15
 
16
16
  def query(db)
17
- db.from(:person_with_dates___p)
17
+ db.from(:person___p)
18
18
  .join(:vocabulary__concept___vc, { vc__concept_id: :p__race_concept_id })
19
19
  .where(Sequel.function(:lower, :vc__concept_name) => arguments.map(&:downcase))
20
20
  end
@@ -0,0 +1,34 @@
1
+ require_relative 'pass_thru'
2
+
3
+ module ConceptQL
4
+ module Nodes
5
+ # Mimics using a variable that has been set via "define" node
6
+ #
7
+ # The idea is that a concept might be very complex and it helps to break
8
+ # that complex concept into a set of sub-concepts to better understand it.
9
+ #
10
+ # This node will look for a sub-concept that has been created through the
11
+ # "define" node and will fetch the results cached in the corresponding table
12
+ class Recall < Node
13
+ # Behind the scenes we simply fetch all rows from the temp table that
14
+ # corresponds to the name fed to "recall"
15
+ #
16
+ # We also set the @types variable by pulling the type information out
17
+ # of the hash piggybacking on the database connection.
18
+ #
19
+ # TODO: This might be an issue since we might need the type information
20
+ # before we call #query. Probably time to reevaluate how we're caching
21
+ # the type information.
22
+ def query(db)
23
+ table_name = namify(arguments.first)
24
+ @types = db.types[table_name]
25
+ db.from(table_name)
26
+ end
27
+
28
+ def types
29
+ @types
30
+ end
31
+ end
32
+ end
33
+ end
34
+
@@ -32,11 +32,12 @@ module ConceptQL
32
32
  .where(Sequel.expr(scm__source_code: values, scm__source_vocabulary_id: vocabulary_id).&(Sequel.expr(scm__source_code: table_source_column)))
33
33
  end
34
34
 
35
- def types
36
- [table]
35
+ def type
36
+ table
37
37
  end
38
38
 
39
39
  private
40
+
40
41
  def table_name
41
42
  @table_name ||= make_table_name(table)
42
43
  end
@@ -25,11 +25,11 @@ module ConceptQL
25
25
  .where(c__concept_code: values, c__vocabulary_id: vocabulary_id)
26
26
  end
27
27
 
28
- def types
29
- [table]
28
+ def type
29
+ table
30
30
  end
31
-
32
31
  private
32
+
33
33
  def table_name
34
34
  @table_name ||= make_table_name(table)
35
35
  end
@@ -9,8 +9,8 @@ module ConceptQL
9
9
  # Proc.new { l.end_date < r.start_date }
10
10
  class TemporalNode < BinaryOperatorNode
11
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])
12
+ db.from(db.from(left_stream(db))
13
+ .join(right_stream(db), [:person_id])
14
14
  .where(where_clause)
15
15
  .select_all(:l))
16
16
  end
@@ -18,6 +18,14 @@ module ConceptQL
18
18
  def inclusive?
19
19
  options[:inclusive]
20
20
  end
21
+
22
+ def left_stream(db)
23
+ Sequel.expr(left.evaluate(db)).as(:l)
24
+ end
25
+
26
+ def right_stream(db)
27
+ Sequel.expr(right.evaluate(db)).as(:r)
28
+ end
21
29
  end
22
30
  end
23
31
  end
@@ -25,7 +25,7 @@ module ConceptQL
25
25
  end
26
26
 
27
27
  private
28
- def date_columns
28
+ def date_columns(type = nil)
29
29
  [adjusted_start_date, adjusted_end_date]
30
30
  end
31
31
 
@@ -1,6 +1,5 @@
1
1
  require 'psych'
2
2
  require_relative 'tree'
3
- require_relative 'view_maker'
4
3
 
5
4
  module ConceptQL
6
5
  class Query
@@ -11,29 +10,34 @@ module ConceptQL
11
10
  @tree = tree
12
11
  end
13
12
 
14
- def query
13
+ def queries
15
14
  build_query(db)
16
15
  end
17
16
 
17
+ def query
18
+ queries.last
19
+ end
20
+
21
+ def sql
22
+ queries.map(&:sql).join(";\n") + ';'
23
+ end
24
+
25
+ # To avoid a performance penalty, only execute the last
26
+ # SQL statement in an array of ConceptQL statements so that define's
27
+ # "create_table" SQL isn't executed twice
18
28
  def execute
19
- ensure_views
20
- build_query(db).all
29
+ query.all
21
30
  end
22
31
 
23
32
  def types
24
- tree.root(self).types
33
+ tree.root(self).last.types
25
34
  end
26
35
 
27
36
  private
28
37
  attr :yaml, :tree, :db
29
38
 
30
39
  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)
40
+ tree.root(self).map { |n| n.evaluate(db) }
37
41
  end
38
42
  end
39
43
  end
@@ -9,8 +9,8 @@ module ConceptQL
9
9
  @behavior = opts.fetch(:behavior, nil)
10
10
  end
11
11
 
12
- def root(query)
13
- @root ||= traverse(query.statement.deep_symbolize_keys)
12
+ def root(*queries)
13
+ @root ||= traverse(queries.flatten.map(&:statement).flatten.map(&:deep_symbolize_keys))
14
14
  end
15
15
 
16
16
  private
@@ -1,3 +1,3 @@
1
1
  module ConceptQL
2
- VERSION = "0.0.3"
2
+ VERSION = "0.0.4"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: conceptql
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Duryea
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-08-12 00:00:00.000000000 Z
11
+ date: 2014-08-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -175,6 +175,8 @@ files:
175
175
  - lib/conceptql/nodes/cpt.rb
176
176
  - lib/conceptql/nodes/date_range.rb
177
177
  - lib/conceptql/nodes/death.rb
178
+ - lib/conceptql/nodes/define.rb
179
+ - lib/conceptql/nodes/drug_type_concept.rb
178
180
  - lib/conceptql/nodes/during.rb
179
181
  - lib/conceptql/nodes/except.rb
180
182
  - lib/conceptql/nodes/first.rb
@@ -195,6 +197,7 @@ files:
195
197
  - lib/conceptql/nodes/place_of_service_code.rb
196
198
  - lib/conceptql/nodes/procedure_occurrence.rb
197
199
  - lib/conceptql/nodes/race.rb
200
+ - lib/conceptql/nodes/recall.rb
198
201
  - lib/conceptql/nodes/rxnorm.rb
199
202
  - lib/conceptql/nodes/snomed.rb
200
203
  - lib/conceptql/nodes/source_vocabulary_node.rb
@@ -209,7 +212,6 @@ files:
209
212
  - lib/conceptql/query.rb
210
213
  - lib/conceptql/tree.rb
211
214
  - lib/conceptql/version.rb
212
- - lib/conceptql/view_maker.rb
213
215
  - spec/conceptql/behaviors/dottable_spec.rb
214
216
  - spec/conceptql/date_adjuster_spec.rb
215
217
  - spec/conceptql/nodes/after_spec.rb
@@ -1,56 +0,0 @@
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