conceptql 0.5.0 → 0.6.0
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 +4 -4
- data/CHANGELOG.md +454 -0
- data/config/vocabularies.csv +85 -87
- data/config/vocabulary_formats.sha +1 -0
- data/config/vocabulary_formats.tsv +39 -0
- data/doc/spec.md +207 -67
- data/lib/conceptql/behaviors/metadatable.rb +15 -3
- data/lib/conceptql/behaviors/provenanceable.rb +82 -8
- data/lib/conceptql/behaviors/utilizable.rb +5 -1
- data/lib/conceptql/data_model/base.rb +2 -4
- data/lib/conceptql/data_model/gdm.rb +3 -1
- data/lib/conceptql/database.rb +7 -2
- data/lib/conceptql/date_adjuster.rb +12 -5
- data/lib/conceptql/lexicon/lexicon_gdm.rb +4 -1
- data/lib/conceptql/lexicon/lexicon_ohdsi.rb +19 -1
- data/lib/conceptql/lexicon/lexicon_strategy.rb +35 -1
- data/lib/conceptql/lexicon.rb +210 -2
- data/lib/conceptql/nodifier.rb +7 -1
- data/lib/conceptql/one_query_counts.rb +120 -0
- data/lib/conceptql/operators/count.rb +4 -0
- data/lib/conceptql/operators/death.rb +1 -1
- data/lib/conceptql/operators/dedup.rb +1 -1
- data/lib/conceptql/operators/episode.rb +10 -2
- data/lib/conceptql/operators/ethnicity.rb +0 -4
- data/lib/conceptql/operators/except.rb +5 -0
- data/lib/conceptql/operators/filter.rb +12 -2
- data/lib/conceptql/operators/hospice.rb +1 -1
- data/lib/conceptql/operators/hospitalization.rb +1 -1
- data/lib/conceptql/operators/information_periods.rb +4 -1
- data/lib/conceptql/operators/invalid.rb +7 -1
- data/lib/conceptql/operators/multiple_vocabularies.rb +19 -6
- data/lib/conceptql/operators/numeric.rb +51 -14
- data/lib/conceptql/operators/occurrence.rb +16 -4
- data/lib/conceptql/operators/operator.rb +63 -19
- data/lib/conceptql/operators/person.rb +1 -1
- data/lib/conceptql/operators/person_filter.rb +5 -0
- data/lib/conceptql/operators/place_of_service_filter.rb +38 -10
- data/lib/conceptql/operators/provenance.rb +12 -2
- data/lib/conceptql/operators/provider_filter.rb +5 -1
- data/lib/conceptql/operators/race.rb +0 -4
- data/lib/conceptql/operators/recall.rb +1 -1
- data/lib/conceptql/operators/snf.rb +2 -2
- data/lib/conceptql/operators/sum.rb +46 -5
- data/lib/conceptql/operators/temporal_operator.rb +6 -2
- data/lib/conceptql/operators/trim_date.rb +52 -6
- data/lib/conceptql/operators/trim_date_end.rb +11 -1
- data/lib/conceptql/operators/trim_date_start.rb +11 -1
- data/lib/conceptql/operators/union.rb +45 -1
- data/lib/conceptql/operators/vocabulary.rb +7 -3
- data/lib/conceptql/paths.rb +10 -0
- data/lib/conceptql/query.rb +78 -24
- data/lib/conceptql/query_modifiers/gdm/provider_query_modifier.rb +7 -7
- data/lib/conceptql/rdbms/generic.rb +21 -1
- data/lib/conceptql/rdbms/spark.rb +12 -0
- data/lib/conceptql/scope.rb +187 -24
- data/lib/conceptql/version.rb +1 -1
- data/lib/conceptql/vocabularies/drg_systems.rb +118 -0
- data/lib/conceptql/vocabularies/dynamic_vocabularies.rb +10 -4
- data/lib/conceptql/vocabularies/entry.rb +15 -3
- data/lib/conceptql/vocabularies/formats.rb +59 -0
- data/lib/conceptql/vocabularies/vocabulary_class_factory.rb +6 -1
- data/lib/conceptql/window/table.rb +12 -3
- metadata +8 -4
- data/lib/conceptql/operators/concurrent_within.rb +0 -69
|
@@ -14,6 +14,18 @@ module ConceptQL
|
|
|
14
14
|
#
|
|
15
15
|
# If the RHS record's start_date is later than the LHS end_date, the LHS
|
|
16
16
|
# record is passed thru unaffected.
|
|
17
|
+
#
|
|
18
|
+
# With join_columns (only t_shank gives it), "per person" means per
|
|
19
|
+
# person and join_columns values, with or without compare_all.
|
|
20
|
+
#
|
|
21
|
+
# With compare_all (only t_shank gives it), each LHS record is compared
|
|
22
|
+
# with every RHS record of the person and of the join_columns, not only
|
|
23
|
+
# with the person's first (trim_date_end) or last (trim_date_start) RHS
|
|
24
|
+
# record. Each LHS record comes out once, trimmed by the nearest RHS
|
|
25
|
+
# record that passes: the earliest start_date on or after its start_date
|
|
26
|
+
# (trim_date_end), or the latest end_date on or before its end_date
|
|
27
|
+
# (trim_date_start). An LHS record is discarded only when it has RHS
|
|
28
|
+
# records and none of them passes.
|
|
17
29
|
class TrimDate < TemporalOperator
|
|
18
30
|
desc <<~EOF
|
|
19
31
|
Trims the end_date of the left hand records (LHR) by the earliest
|
|
@@ -38,6 +50,8 @@ module ConceptQL
|
|
|
38
50
|
.left_join(Sequel.as(right_stream_query(db), :r), join_columns.inject(&:&))
|
|
39
51
|
.where(where_criteria)
|
|
40
52
|
|
|
53
|
+
ds = nearest_trim_date(ds) if compare_all?
|
|
54
|
+
|
|
41
55
|
ds = dm.selectify(ds, qualifier: :l, replace: replacement_columns)
|
|
42
56
|
|
|
43
57
|
ds = apply_selectors(ds)
|
|
@@ -51,16 +65,48 @@ module ConceptQL
|
|
|
51
65
|
options[:compare_all]
|
|
52
66
|
end
|
|
53
67
|
|
|
68
|
+
# With compare_all, each left row joins every right row of the person
|
|
69
|
+
# (and of the join columns), and the where clause keeps the right rows
|
|
70
|
+
# that pass. Group those back to one row for each left row, and keep
|
|
71
|
+
# the nearest passing trim date (trim_function). A left row without a
|
|
72
|
+
# right row has a NULL trim date, so it passes through unchanged (bead
|
|
73
|
+
# conceptql-x6m, seed conceptql-ysx). Every stream is a set, so left
|
|
74
|
+
# rows that are equal in every column come out once (seed conceptql-0eb).
|
|
75
|
+
def nearest_trim_date(ds)
|
|
76
|
+
ds.select_group(*dynamic_columns.map { |column| Sequel[:l][column] })
|
|
77
|
+
.select_append(Sequel.function(trim_function, trim_bound).as(:trim_bound))
|
|
78
|
+
.from_self(alias: :l)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# The trim date of the joined right row, or the nearest one of the left
|
|
82
|
+
# row when compare_all grouped the right rows.
|
|
83
|
+
def trim_date_column
|
|
84
|
+
compare_all? ? Sequel[:l][:trim_bound] : trim_bound
|
|
85
|
+
end
|
|
86
|
+
|
|
54
87
|
def right_stream_query(db)
|
|
55
|
-
rh =
|
|
56
|
-
super
|
|
57
|
-
else
|
|
58
|
-
occ = to_op([ :occurrence, occurrence_number, right ])
|
|
59
|
-
occ.evaluate(db)
|
|
60
|
-
end
|
|
88
|
+
rh = compare_all? ? super : nearest_right_row(db)
|
|
61
89
|
columnizer.apply(rh)
|
|
62
90
|
end
|
|
63
91
|
|
|
92
|
+
# Without compare_all, the first (trim_date_end) or last
|
|
93
|
+
# (trim_date_start) right row of each person and join_columns values,
|
|
94
|
+
# the grouping compare_all uses. The rows are ordered as the occurrence
|
|
95
|
+
# operator orders them: by start_date, then criterion_id. The occurrence
|
|
96
|
+
# operator groups by person only, so a left row whose join values
|
|
97
|
+
# differed from those of the person's first right row passed through
|
|
98
|
+
# untrimmed (bead conceptql-18n).
|
|
99
|
+
def nearest_right_row(db)
|
|
100
|
+
direction = occurrence_number.negative? ? :desc : :asc
|
|
101
|
+
right.evaluate(db)
|
|
102
|
+
.select_append(Sequel[:ROW_NUMBER].function.over(
|
|
103
|
+
partition: join_columns_option.uniq,
|
|
104
|
+
order: %i[start_date criterion_id].map { |column| Sequel.send(direction, column) }
|
|
105
|
+
).as(:rn))
|
|
106
|
+
.from_self
|
|
107
|
+
.where(rn: occurrence_number.abs)
|
|
108
|
+
end
|
|
109
|
+
|
|
64
110
|
def columnizer
|
|
65
111
|
columnizer = Columnizer.new
|
|
66
112
|
columnizer.add_columns(:person_id, trim_date, *join_columns_option)
|
|
@@ -32,7 +32,17 @@ module ConceptQL
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def replacement_columns
|
|
35
|
-
{ end_date: Sequel.function(:least, l_end_date, Sequel.function(:coalesce,
|
|
35
|
+
{ end_date: Sequel.function(:least, l_end_date, Sequel.function(:coalesce, trim_date_column, l_end_date)) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The date that trims a left row, from one right row.
|
|
39
|
+
def trim_bound
|
|
40
|
+
within_start
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# With compare_all, the nearest trim date of the right rows that pass.
|
|
44
|
+
def trim_function
|
|
45
|
+
:min
|
|
36
46
|
end
|
|
37
47
|
|
|
38
48
|
def occurrence_number
|
|
@@ -32,7 +32,17 @@ module ConceptQL
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def replacement_columns
|
|
35
|
-
{ start_date: Sequel.function(:greatest, l_start_date, Sequel.function(:coalesce,
|
|
35
|
+
{ start_date: Sequel.function(:greatest, l_start_date, Sequel.function(:coalesce, trim_date_column, l_start_date)) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The date that trims a left row, from one right row.
|
|
39
|
+
def trim_bound
|
|
40
|
+
within_end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# With compare_all, the nearest trim date of the right rows that pass.
|
|
44
|
+
def trim_function
|
|
45
|
+
:max
|
|
36
46
|
end
|
|
37
47
|
|
|
38
48
|
def occurrence_number
|
|
@@ -70,11 +70,55 @@ module ConceptQL
|
|
|
70
70
|
|
|
71
71
|
queries += Combiner.new(combinables).queries(db)
|
|
72
72
|
|
|
73
|
+
all = disjoint_upstreams?
|
|
73
74
|
queries.map! { |ds| ds.from_self.select(*query_cols).from_self }
|
|
74
75
|
queries.inject do |q, query|
|
|
75
|
-
q.union(query, all:
|
|
76
|
+
q.union(query, all: all)
|
|
76
77
|
end
|
|
77
78
|
end
|
|
79
|
+
|
|
80
|
+
# The (criterion_domain, vocabulary condition) pairs of every row this
|
|
81
|
+
# union can emit, or nil when an upstream does not fix them at build
|
|
82
|
+
# time. See #disjoint_upstreams?.
|
|
83
|
+
def branches
|
|
84
|
+
upstream_branches = upstreams.map { |upstream| branches_of(upstream) }
|
|
85
|
+
upstream_branches.flatten(1) if upstream_branches.all?
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
# Every stream is a set (seed conceptql-0eb), so union removes the rows
|
|
91
|
+
# that arrive from more than one upstream: UNION, not UNION ALL. The
|
|
92
|
+
# duplicate removal is expensive on large data (conceptql-5pz), so union
|
|
93
|
+
# skips it when no row can arrive from two of its queries. It proves
|
|
94
|
+
# that only for vocabulary operators, cpt_or_hcpcs-style operators and
|
|
95
|
+
# unions of them, because each of their rows carries the operator's
|
|
96
|
+
# domain as criterion_domain and comes from a row of its vocabulary.
|
|
97
|
+
# Two rows with different domains differ, and so do two rows of one
|
|
98
|
+
# domain (one table) from different vocabularies of one column. Any
|
|
99
|
+
# other upstream can overlap anything, so union removes duplicates.
|
|
100
|
+
def disjoint_upstreams?
|
|
101
|
+
combinables, individuals = upstreams.partition { |upstream| upstream.is_a?(Vocabulary) }
|
|
102
|
+
queries = individuals.map { |upstream| branches_of(upstream) }
|
|
103
|
+
queries += combinables.group_by(&:domain).values.map { |combos| combos.flat_map { |c| branches_of(c) } }
|
|
104
|
+
return false unless queries.all?
|
|
105
|
+
|
|
106
|
+
queries.combination(2).all? do |one, other|
|
|
107
|
+
one.product(other).all? { |a, b| disjoint_branches?(a, b) }
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def branches_of(upstream)
|
|
112
|
+
case upstream
|
|
113
|
+
when Vocabulary then [ [ upstream.domain, upstream.vocabulary_condition ] ]
|
|
114
|
+
when MultipleVocabularies then upstream.vocab_ops.flat_map { |vo| branches_of(vo) }
|
|
115
|
+
when Union then upstream.branches
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def disjoint_branches?((domain, condition), (other_domain, other_condition))
|
|
120
|
+
domain != other_domain || (condition.keys == other_condition.keys && condition != other_condition)
|
|
121
|
+
end
|
|
78
122
|
end
|
|
79
123
|
end
|
|
80
124
|
end
|
|
@@ -38,7 +38,7 @@ module ConceptQL
|
|
|
38
38
|
args -= bad_arguments
|
|
39
39
|
|
|
40
40
|
missing_args = args - dm.known_codes(db, vocabulary_id, args)
|
|
41
|
-
add_warning(
|
|
41
|
+
add_warning(UNKNOWN_CODE, *missing_args) unless missing_args.empty?
|
|
42
42
|
|
|
43
43
|
warn_about_reused_codes(db, args - missing_args)
|
|
44
44
|
end
|
|
@@ -137,13 +137,17 @@ module ConceptQL
|
|
|
137
137
|
unless defined?(@code_regexp)
|
|
138
138
|
@code_regexp = nil
|
|
139
139
|
|
|
140
|
-
if (
|
|
141
|
-
@code_regexp = Regexp.union(
|
|
140
|
+
if (format = Vocabularies::Formats.regexp(vocab_entry.omopv5_id))
|
|
141
|
+
@code_regexp = Regexp.union(format, /\A\*\Z/)
|
|
142
142
|
end
|
|
143
143
|
end
|
|
144
144
|
@code_regexp
|
|
145
145
|
end
|
|
146
146
|
|
|
147
|
+
def bad_format_message
|
|
148
|
+
vocab_entry.omopv5_id == "NDC" ? NDC_BAD_FORMAT : BAD_FORMAT
|
|
149
|
+
end
|
|
150
|
+
|
|
147
151
|
def vocabulary_id
|
|
148
152
|
@vocabulary_id ||= translated_vocabulary_id
|
|
149
153
|
end
|
data/lib/conceptql/paths.rb
CHANGED
|
@@ -22,6 +22,16 @@ module ConceptQL
|
|
|
22
22
|
ConceptQL.config_dir / "vocabularies.csv"
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
# A pinned copy of vocabulary_formats.tsv (ConceptQL::Vocabularies::Formats)
|
|
26
|
+
def vocabulary_formats_file_path
|
|
27
|
+
ConceptQL.config_dir / "vocabulary_formats.tsv"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The vocabulary_formats commit that the copy came from
|
|
31
|
+
def vocabulary_formats_sha_file_path
|
|
32
|
+
ConceptQL.config_dir / "vocabulary_formats.sha"
|
|
33
|
+
end
|
|
34
|
+
|
|
25
35
|
def multiple_vocabularies_file_path
|
|
26
36
|
ConceptQL.config_dir / "multiple_vocabularies.csv"
|
|
27
37
|
end
|
data/lib/conceptql/query.rb
CHANGED
|
@@ -4,6 +4,7 @@ require "json"
|
|
|
4
4
|
require "forwardable"
|
|
5
5
|
require_relative "scope"
|
|
6
6
|
require_relative "nodifier"
|
|
7
|
+
require_relative "one_query_counts"
|
|
7
8
|
require "sequel/sqlformat"
|
|
8
9
|
|
|
9
10
|
module ConceptQL
|
|
@@ -20,29 +21,39 @@ module ConceptQL
|
|
|
20
21
|
[ super, "OG ERROR:", og_message ].join("\n")
|
|
21
22
|
end
|
|
22
23
|
end
|
|
24
|
+
end
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
26
|
+
# Raised when a Query builds the SQL of a statement (#query, #sql,
|
|
27
|
+
# #sql_statements, and #all, #count, #execute, #order and #analyze through
|
|
28
|
+
# #query) or its code list (#code_list), and the statement has validation
|
|
29
|
+
# errors. An invalid statement can crash while it builds, or build SQL that
|
|
30
|
+
# silently returns fewer rows. Warnings do not raise.
|
|
31
|
+
#
|
|
32
|
+
# #errors is the errors hash that #scope_annotate reports (operator name =>
|
|
33
|
+
# list of [message, *details]), so it holds the errors that annotate shows.
|
|
34
|
+
# #statement is the statement that the Query holds.
|
|
35
|
+
class InvalidStatementError < Query::QueryError
|
|
36
|
+
attr_reader :statement, :errors
|
|
37
|
+
|
|
38
|
+
def initialize(statement, errors)
|
|
39
|
+
@statement = statement
|
|
40
|
+
@errors = errors
|
|
41
|
+
super([ "Invalid ConceptQL statement. Validation errors:",
|
|
42
|
+
*errors.flat_map { |op, op_errors| op_errors.map { |e| " #{op}: #{describe(e)}" } },
|
|
43
|
+
"Statement: #{statement.inspect}" ].join("\n"))
|
|
44
|
+
end
|
|
38
45
|
|
|
39
|
-
|
|
46
|
+
private
|
|
40
47
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
end
|
|
48
|
+
def describe(error)
|
|
49
|
+
message, *details = error
|
|
50
|
+
details.empty? ? message.to_s : "#{message} (#{details.flatten.map(&:to_s).join(', ')})"
|
|
45
51
|
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
class Query
|
|
55
|
+
# The name of InvalidStatementError in 0.4.0 to 0.5.0.
|
|
56
|
+
InvalidStatementError = ConceptQL::InvalidStatementError
|
|
46
57
|
|
|
47
58
|
extend Forwardable
|
|
48
59
|
def_delegators :query, :all, :count, :execute, :order
|
|
@@ -66,7 +77,9 @@ module ConceptQL
|
|
|
66
77
|
query(explain: true, analyze: true).analyze
|
|
67
78
|
end
|
|
68
79
|
|
|
80
|
+
# Raises InvalidStatementError if the statement has validation errors.
|
|
69
81
|
def query(opts = {})
|
|
82
|
+
validate!
|
|
70
83
|
(@queries ||= {})[opts] ||= nodifier.scope.with_ctes(operator, db, opts)
|
|
71
84
|
end
|
|
72
85
|
|
|
@@ -112,11 +125,26 @@ module ConceptQL
|
|
|
112
125
|
end
|
|
113
126
|
end
|
|
114
127
|
Hash[stmts]
|
|
128
|
+
rescue InvalidStatementError
|
|
129
|
+
raise
|
|
115
130
|
rescue StandardError
|
|
116
131
|
raise QueryError, "Failed to generate SQL for #{statement.inspect}"
|
|
117
132
|
end
|
|
118
133
|
|
|
134
|
+
# With counts on, DuckDB and Postgres count every operator with one SQL
|
|
135
|
+
# statement (OneQueryCounts). Operators memoize #annotate, so a later call
|
|
136
|
+
# with other opts returns the first result.
|
|
137
|
+
#
|
|
138
|
+
# The errors of the window table's own statement are reported under
|
|
139
|
+
# "window_table" (see #window_table_errors). Then nothing is counted,
|
|
140
|
+
# because every count joins the window table, and building its SQL raises.
|
|
119
141
|
def annotate(opts = {})
|
|
142
|
+
window_errors = window_table_errors
|
|
143
|
+
unless window_errors.empty?
|
|
144
|
+
nodifier.scope.add_errors("window_table", window_errors)
|
|
145
|
+
opts = opts.merge(skip_counts: true)
|
|
146
|
+
end
|
|
147
|
+
nodifier.scope.one_query_counts = OneQueryCounts.new(operator, db, opts)
|
|
120
148
|
operator.annotate(db, opts)
|
|
121
149
|
end
|
|
122
150
|
|
|
@@ -164,12 +192,11 @@ module ConceptQL
|
|
|
164
192
|
operator.rdbms
|
|
165
193
|
end
|
|
166
194
|
|
|
167
|
-
# Raises InvalidStatementError if the statement has validation errors
|
|
168
|
-
#
|
|
195
|
+
# Raises InvalidStatementError if the statement has validation errors:
|
|
196
|
+
# the operator tree of an invalid statement can have missing operands,
|
|
197
|
+
# and a partial code list would look complete. Warnings do not raise.
|
|
169
198
|
def code_list(_ignored_db = nil)
|
|
170
|
-
|
|
171
|
-
raise InvalidStatementError.new(statement, errors) unless errors.empty?
|
|
172
|
-
|
|
199
|
+
validate!
|
|
173
200
|
operator.code_list(db).uniq
|
|
174
201
|
end
|
|
175
202
|
|
|
@@ -189,6 +216,33 @@ module ConceptQL
|
|
|
189
216
|
|
|
190
217
|
attr_reader :cdb, :nodifier
|
|
191
218
|
|
|
219
|
+
# Raises InvalidStatementError if #validation_errors is not empty. The
|
|
220
|
+
# statement and the opts of a Query do not change, so it checks once.
|
|
221
|
+
# Warnings do not raise. cause: nil keeps an error that a caller is
|
|
222
|
+
# handling out of the message (QueryError#message prints the cause).
|
|
223
|
+
def validate!
|
|
224
|
+
return if @valid
|
|
225
|
+
|
|
226
|
+
errors = validation_errors
|
|
227
|
+
raise InvalidStatementError.new(statement, errors), cause: nil unless errors.empty?
|
|
228
|
+
|
|
229
|
+
@valid = true
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# The validation errors of the window table's statement, when the window
|
|
233
|
+
# table is a ConceptQL statement (an Array, as Window::Table reads it), as
|
|
234
|
+
# one list of [message, *details]. Each message starts with the name of
|
|
235
|
+
# the operator that reported it. A table name has no errors.
|
|
236
|
+
def window_table_errors
|
|
237
|
+
scope_opts = nodifier.scope.opts
|
|
238
|
+
window_table = scope_opts.merge(scope_opts[:window_opts] || {})[:window_table]
|
|
239
|
+
return [] unless window_table.is_a?(Array)
|
|
240
|
+
|
|
241
|
+
cdb.query(window_table).validation_errors.flat_map do |op, op_errors|
|
|
242
|
+
op_errors.map { |message, *details| [ "#{op}: #{message}", *details ] }
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
192
246
|
def extract_statement(stmt, opts)
|
|
193
247
|
if !stmt.is_a?(Array)
|
|
194
248
|
raise "Improper ConceptQL statement: Expected an Array, got a #{stmt.class}"
|
|
@@ -29,20 +29,20 @@ module ConceptQL
|
|
|
29
29
|
Sequel[:p][:specialty_concept_id].as(:practitioner_specialty_concept_id)
|
|
30
30
|
)
|
|
31
31
|
else
|
|
32
|
+
# The data model names the columns of the source table. ds.columns
|
|
33
|
+
# asked the database for them while the SQL was built (bead
|
|
34
|
+
# conceptql-6k8).
|
|
35
|
+
cols = dm.table_cols(source_table)
|
|
32
36
|
ds = query.select_all
|
|
33
|
-
ds = if
|
|
37
|
+
ds = if cols.include?(:provider_id)
|
|
34
38
|
ds
|
|
35
|
-
elsif
|
|
39
|
+
elsif cols.include?(:practitioner_id)
|
|
36
40
|
ds.select_append(Sequel[:practitioner_id].as(:provider_id))
|
|
37
41
|
else
|
|
38
42
|
ds.select_append(Sequel.cast(nil, :Bigint).as(:provider_id))
|
|
39
43
|
end
|
|
40
44
|
%i[role_type_concept_id context_specialty_concept_id practitioner_specialty_concept_id].each do |col|
|
|
41
|
-
ds =
|
|
42
|
-
ds
|
|
43
|
-
else
|
|
44
|
-
ds.select_append(Sequel.cast(nil, :Bigint).as(col))
|
|
45
|
-
end
|
|
45
|
+
ds = ds.select_append(Sequel.cast(nil, :Bigint).as(col)) unless cols.include?(col)
|
|
46
46
|
end
|
|
47
47
|
ds
|
|
48
48
|
end.from_self
|
|
@@ -53,9 +53,13 @@ module ConceptQL
|
|
|
53
53
|
.inject(:+)
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
+
# A NULL part is an empty string. The || operator gives NULL when one
|
|
57
|
+
# part is NULL, and a NULL uuid matches nothing (bead conceptql-18n).
|
|
58
|
+
# COALESCE keeps the place of each part, so "1//t/2008-01-01/" cannot
|
|
59
|
+
# be read as other parts. Postgres, DuckDB and Spark all support it.
|
|
56
60
|
def uuid_items
|
|
57
61
|
%w[person_id criterion_id criterion_table start_date end_date].map do |column|
|
|
58
|
-
Sequel.cast_string(column.to_sym)
|
|
62
|
+
Sequel.function(:coalesce, Sequel.cast_string(column.to_sym), "")
|
|
59
63
|
end
|
|
60
64
|
end
|
|
61
65
|
|
|
@@ -79,6 +83,22 @@ module ConceptQL
|
|
|
79
83
|
false
|
|
80
84
|
end
|
|
81
85
|
|
|
86
|
+
# True if an IN subquery may sit in a SELECT-list expression, such as
|
|
87
|
+
# the CASE flags of One In Two Out. Scope#with_ctes reads a long IN list
|
|
88
|
+
# from its list CTE there only when this is true.
|
|
89
|
+
def supports_in_subquery_in_select?
|
|
90
|
+
true
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# True if a NOT IN subquery may sit in a nested condition, such as the
|
|
94
|
+
# "IS NULL OR NOT IN" of the gender operator. Scope#with_ctes reads a
|
|
95
|
+
# long NOT IN list from its list CTE only when this is true. It does not
|
|
96
|
+
# check whether a NOT IN condition is nested, so false keeps every long
|
|
97
|
+
# NOT IN list a NOT IN list.
|
|
98
|
+
def supports_nested_not_in_subquery?
|
|
99
|
+
true
|
|
100
|
+
end
|
|
101
|
+
|
|
82
102
|
def create_options(_scope, _ds)
|
|
83
103
|
opts = {}
|
|
84
104
|
opts[:analyze] = opts[:explain] = explain_temp_tables?
|
|
@@ -13,6 +13,18 @@ module ConceptQL
|
|
|
13
13
|
def null_safe_equal(lhs, rhs)
|
|
14
14
|
Sequel.lit("(? <=> ?)", lhs, rhs)
|
|
15
15
|
end
|
|
16
|
+
|
|
17
|
+
# Spark accepts an IN subquery only in a filter or a join: "IN/EXISTS
|
|
18
|
+
# predicate sub-queries can only be used in Filter/Join" (Spark 3.3).
|
|
19
|
+
def supports_in_subquery_in_select?
|
|
20
|
+
false
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Spark rejects a NOT IN subquery in a nested condition: "Null-aware
|
|
24
|
+
# predicate sub-queries cannot be used in nested conditions" (Spark 3.3).
|
|
25
|
+
def supports_nested_not_in_subquery?
|
|
26
|
+
false
|
|
27
|
+
end
|
|
16
28
|
end
|
|
17
29
|
end
|
|
18
30
|
end
|