conceptql 0.2.0 → 0.4.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 +5 -5
- data/CHANGELOG.md +226 -117
- data/LICENSE +21 -0
- data/README.md +35 -83
- data/bin/conceptql +2 -1
- data/bin/conceptql_utils +6 -0
- data/config/multiple_vocabularies.csv +4 -0
- data/config/provenance.yml +489 -0
- data/config/race.yml +50 -0
- data/config/vocabularies.csv +87 -0
- data/lib/conceptql/behaviors/code_lister.rb +23 -0
- data/lib/conceptql/behaviors/drugish.rb +17 -0
- data/lib/conceptql/behaviors/labish.rb +19 -0
- data/lib/conceptql/behaviors/metadatable.rb +226 -73
- data/lib/conceptql/behaviors/provenanceable.rb +26 -0
- data/lib/conceptql/behaviors/single_event.rb +11 -0
- data/lib/conceptql/behaviors/timeless.rb +11 -0
- data/lib/conceptql/behaviors/utilizable.rb +206 -0
- data/lib/conceptql/behaviors/windowable.rb +27 -0
- data/lib/conceptql/cli.rb +117 -108
- data/lib/conceptql/columnizer.rb +75 -0
- data/lib/conceptql/data_model/base.rb +462 -0
- data/lib/conceptql/data_model/gdm.rb +155 -0
- data/lib/conceptql/data_model/gdm_wide.rb +42 -0
- data/lib/conceptql/data_model/omopv4_plus.rb +12 -0
- data/lib/conceptql/data_model.rb +30 -0
- data/lib/conceptql/database.rb +214 -0
- data/lib/conceptql/date_adjuster.rb +66 -18
- data/lib/conceptql/diagram.rb +243 -0
- data/lib/conceptql/lexicon/lexicon_gdm.rb +47 -0
- data/lib/conceptql/lexicon/lexicon_no_db.rb +49 -0
- data/lib/conceptql/lexicon/lexicon_ohdsi.rb +63 -0
- data/lib/conceptql/lexicon/lexicon_strategy.rb +116 -0
- data/lib/conceptql/lexicon.rb +111 -0
- data/lib/conceptql/nodifier.rb +65 -24
- data/lib/conceptql/operators/after.rb +34 -14
- data/lib/conceptql/operators/any_overlap.rb +8 -6
- data/lib/conceptql/operators/before.rb +34 -10
- data/lib/conceptql/operators/binary_operator_operator.rb +66 -23
- data/lib/conceptql/operators/casting_operator.rb +51 -36
- data/lib/conceptql/operators/co_reported.rb +111 -0
- data/lib/conceptql/operators/concurrent_within.rb +69 -0
- data/lib/conceptql/operators/contains.rb +12 -8
- data/lib/conceptql/operators/count.rb +17 -9
- data/lib/conceptql/operators/date_range.rb +48 -14
- data/lib/conceptql/operators/death.rb +14 -4
- data/lib/conceptql/operators/dedup.rb +31 -0
- data/lib/conceptql/operators/during.rb +7 -12
- data/lib/conceptql/operators/episode.rb +146 -0
- data/lib/conceptql/operators/equal.rb +9 -3
- data/lib/conceptql/operators/ethnicity.rb +49 -0
- data/lib/conceptql/operators/except.rb +19 -12
- data/lib/conceptql/operators/filter.rb +23 -8
- data/lib/conceptql/operators/first.rb +16 -3
- data/lib/conceptql/operators/from.rb +52 -4
- data/lib/conceptql/operators/gender.rb +34 -11
- data/lib/conceptql/operators/hospice.rb +19 -0
- data/lib/conceptql/operators/hospitalization.rb +19 -0
- data/lib/conceptql/operators/information_periods.rb +36 -0
- data/lib/conceptql/operators/invalid.rb +35 -0
- data/lib/conceptql/operators/last.rb +16 -3
- data/lib/conceptql/operators/match.rb +91 -0
- data/lib/conceptql/operators/multiple_vocabularies.rb +172 -0
- data/lib/conceptql/operators/numeric.rb +65 -23
- data/lib/conceptql/operators/numeric_filter.rb +68 -0
- data/lib/conceptql/operators/occurrence.rb +153 -32
- data/lib/conceptql/operators/one_in_two_out.rb +237 -38
- data/lib/conceptql/operators/operator.rb +624 -178
- data/lib/conceptql/operators/pass_thru.rb +17 -3
- data/lib/conceptql/operators/person.rb +18 -6
- data/lib/conceptql/operators/person_filter.rb +9 -3
- data/lib/conceptql/operators/place_of_service_filter.rb +43 -0
- data/lib/conceptql/operators/provenance.rb +55 -0
- data/lib/conceptql/operators/provider_filter.rb +62 -0
- data/lib/conceptql/operators/race.rb +55 -7
- data/lib/conceptql/operators/read.rb +168 -0
- data/lib/conceptql/operators/recall.rb +49 -14
- data/lib/conceptql/operators/snf.rb +19 -0
- data/lib/conceptql/operators/sum.rb +29 -12
- data/lib/conceptql/operators/temporal_operator.rb +108 -16
- data/lib/conceptql/operators/time_window.rb +20 -32
- data/lib/conceptql/operators/trim_date.rb +71 -0
- data/lib/conceptql/operators/trim_date_end.rb +28 -40
- data/lib/conceptql/operators/trim_date_start.rb +28 -41
- data/lib/conceptql/operators/union.rb +67 -6
- data/lib/conceptql/operators/vocabulary.rb +151 -0
- data/lib/conceptql/paths.rb +41 -0
- data/lib/conceptql/query.rb +191 -26
- data/lib/conceptql/query_modifiers/gdm/admission_date_query_modifier.rb +46 -0
- data/lib/conceptql/query_modifiers/gdm/drug_query_modifier.rb +54 -0
- data/lib/conceptql/query_modifiers/gdm/lab_query_modifier.rb +57 -0
- data/lib/conceptql/query_modifiers/gdm/pos_query_modifier.rb +46 -0
- data/lib/conceptql/query_modifiers/gdm/provenance_query_modifier.rb +45 -0
- data/lib/conceptql/query_modifiers/gdm/provider_query_modifier.rb +53 -0
- data/lib/conceptql/query_modifiers/gdm_wide/admission_date_query_modifier.rb +42 -0
- data/lib/conceptql/query_modifiers/gdm_wide/drug_query_modifier.rb +52 -0
- data/lib/conceptql/query_modifiers/gdm_wide/lab_query_modifier.rb +55 -0
- data/lib/conceptql/query_modifiers/gdm_wide/pos_query_modifier.rb +37 -0
- data/lib/conceptql/query_modifiers/gdm_wide/provenance_query_modifier.rb +38 -0
- data/lib/conceptql/query_modifiers/omopv4_plus/admission_date_query_modifier.rb +45 -0
- data/lib/conceptql/query_modifiers/omopv4_plus/drug_query_modifier.rb +80 -0
- data/lib/conceptql/query_modifiers/omopv4_plus/pos_query_modifier.rb +47 -0
- data/lib/conceptql/query_modifiers/omopv4_plus/provenance_query_modifier.rb +45 -0
- data/lib/conceptql/query_modifiers/omopv4_plus/provider_query_modifier.rb +26 -0
- data/lib/conceptql/query_modifiers/query_modifier.rb +20 -0
- data/lib/conceptql/rdbms/duckdb.rb +25 -0
- data/lib/conceptql/rdbms/generic.rb +120 -0
- data/lib/conceptql/rdbms/postgres.rb +51 -0
- data/lib/conceptql/rdbms/spark.rb +22 -0
- data/lib/conceptql/rdbms.rb +22 -0
- data/lib/conceptql/scope.rb +481 -31
- data/lib/conceptql/sql_formatters/formatter.rb +25 -0
- data/lib/conceptql/sql_formatters.rb +22 -0
- data/lib/conceptql/utils.rb +113 -0
- data/lib/conceptql/utils_cli.rb +17 -0
- data/lib/conceptql/version.rb +3 -1
- data/lib/conceptql/visitors/metadata_extractor.rb +54 -0
- data/lib/conceptql/vocabularies/behaviors/costish.rb +24 -0
- data/lib/conceptql/vocabularies/behaviors/gdmish.rb +19 -0
- data/lib/conceptql/vocabularies/behaviors/omopish.rb +32 -0
- data/lib/conceptql/vocabularies/dynamic_vocabularies.rb +63 -0
- data/lib/conceptql/vocabularies/entry.rb +138 -0
- data/lib/conceptql/vocabularies/vocabulary_class_factory.rb +85 -0
- data/lib/conceptql/window/base.rb +26 -0
- data/lib/conceptql/window/date_range.rb +25 -0
- data/lib/conceptql/window/table.rb +116 -0
- data/lib/conceptql/window.rb +72 -0
- data/lib/conceptql.rb +65 -2
- data/lib/sequel/extensions/pg_ctas_explain.rb +22 -0
- data/lib/sequel/extensions/pg_vacuum_table.rb +23 -0
- data/schemas/gdm.yml +882 -0
- data/schemas/gdm_wide.yml +757 -0
- data/schemas/ohdsi_vocabs.yml +143 -0
- data/schemas/omopv4_plus.yml +729 -0
- data/schemas/omopv4_plus_more_ignorables.tsv +13 -0
- metadata +267 -207
- data/.gitignore +0 -23
- data/Gemfile +0 -4
- data/Guardfile +0 -56
- data/LICENSE.txt +0 -22
- data/Rakefile +0 -1
- data/conceptql.gemspec +0 -32
- data/doc/ConceptQL Specification (alpha).pdf +0 -0
- data/doc/diagram_0.png +0 -0
- data/doc/implementation_notes.md +0 -39
- data/doc/spec.md +0 -1344
- data/lib/conceptql/behaviors/debuggable.rb +0 -70
- data/lib/conceptql/behaviors/dottable.rb +0 -103
- data/lib/conceptql/behaviors/preppable.rb +0 -20
- data/lib/conceptql/converter.rb +0 -65
- data/lib/conceptql/debugger.rb +0 -48
- data/lib/conceptql/fake_grapher.rb +0 -20
- data/lib/conceptql/graph.rb +0 -52
- data/lib/conceptql/graph_nodifier.rb +0 -203
- data/lib/conceptql/logger.rb +0 -10
- data/lib/conceptql/operators/complement.rb +0 -38
- data/lib/conceptql/operators/concept.rb +0 -70
- data/lib/conceptql/operators/condition_type.rb +0 -71
- data/lib/conceptql/operators/cpt.rb +0 -25
- data/lib/conceptql/operators/drug_type_concept.rb +0 -21
- data/lib/conceptql/operators/from_seer_visits.rb +0 -23
- data/lib/conceptql/operators/hcpcs.rb +0 -25
- data/lib/conceptql/operators/icd10.rb +0 -28
- data/lib/conceptql/operators/icd9.rb +0 -28
- data/lib/conceptql/operators/icd9_procedure.rb +0 -25
- data/lib/conceptql/operators/intersect.rb +0 -34
- data/lib/conceptql/operators/loinc.rb +0 -25
- data/lib/conceptql/operators/medcode.rb +0 -28
- data/lib/conceptql/operators/medcode_procedure.rb +0 -27
- data/lib/conceptql/operators/ndc.rb +0 -29
- data/lib/conceptql/operators/observation_by_enttype.rb +0 -29
- data/lib/conceptql/operators/observation_period.rb +0 -30
- data/lib/conceptql/operators/overlapped_by.rb +0 -23
- data/lib/conceptql/operators/overlaps.rb +0 -19
- data/lib/conceptql/operators/place_of_service_code.rb +0 -24
- data/lib/conceptql/operators/procedure_occurrence.rb +0 -25
- data/lib/conceptql/operators/prodcode.rb +0 -29
- data/lib/conceptql/operators/rxnorm.rb +0 -24
- data/lib/conceptql/operators/snomed.rb +0 -24
- data/lib/conceptql/operators/snomed_condition.rb +0 -26
- data/lib/conceptql/operators/source_vocabulary_operator.rb +0 -58
- data/lib/conceptql/operators/standard_vocabulary_operator.rb +0 -45
- data/lib/conceptql/operators/started_by.rb +0 -22
- data/lib/conceptql/operators/to_seer_visits.rb +0 -24
- data/lib/conceptql/operators/visit.rb +0 -15
- data/lib/conceptql/operators/visit_occurrence.rb +0 -30
- data/lib/conceptql/tree.rb +0 -53
- data/lib/conceptql/utils/temp_table.rb +0 -72
- data/spec/conceptql/behaviors/dottable_spec.rb +0 -99
- data/spec/conceptql/converter_spec.rb +0 -51
- data/spec/conceptql/date_adjuster_spec.rb +0 -68
- data/spec/conceptql/operators/after_spec.rb +0 -16
- data/spec/conceptql/operators/before_spec.rb +0 -16
- data/spec/conceptql/operators/casting_operator_spec.rb +0 -68
- data/spec/conceptql/operators/complement_spec.rb +0 -15
- data/spec/conceptql/operators/concept_spec.rb +0 -40
- data/spec/conceptql/operators/condition_type_spec.rb +0 -128
- data/spec/conceptql/operators/contains_spec.rb +0 -19
- data/spec/conceptql/operators/cpt_spec.rb +0 -29
- data/spec/conceptql/operators/date_range_spec.rb +0 -33
- data/spec/conceptql/operators/death_spec.rb +0 -10
- data/spec/conceptql/operators/during_spec.rb +0 -30
- data/spec/conceptql/operators/except_spec.rb +0 -15
- data/spec/conceptql/operators/first_spec.rb +0 -35
- data/spec/conceptql/operators/from_spec.rb +0 -13
- data/spec/conceptql/operators/gender_spec.rb +0 -27
- data/spec/conceptql/operators/hcpcs_spec.rb +0 -29
- data/spec/conceptql/operators/icd10_spec.rb +0 -34
- data/spec/conceptql/operators/icd9_procedure_spec.rb +0 -29
- data/spec/conceptql/operators/icd9_spec.rb +0 -34
- data/spec/conceptql/operators/intersect_spec.rb +0 -28
- data/spec/conceptql/operators/last_spec.rb +0 -36
- data/spec/conceptql/operators/loinc_spec.rb +0 -29
- data/spec/conceptql/operators/medcode_procedure_spec.rb +0 -34
- data/spec/conceptql/operators/medcode_spec.rb +0 -34
- data/spec/conceptql/operators/observation_period_spec.rb +0 -10
- data/spec/conceptql/operators/occurrence_spec.rb +0 -87
- data/spec/conceptql/operators/overlapped_by_spec.rb +0 -32
- data/spec/conceptql/operators/overlaps_spec.rb +0 -21
- data/spec/conceptql/operators/person_filter_spec.rb +0 -15
- data/spec/conceptql/operators/person_spec.rb +0 -10
- data/spec/conceptql/operators/place_of_service_code_spec.rb +0 -24
- data/spec/conceptql/operators/procedure_occurrence_spec.rb +0 -10
- data/spec/conceptql/operators/prodcode_spec.rb +0 -35
- data/spec/conceptql/operators/query_double.rb +0 -20
- data/spec/conceptql/operators/query_double_spec.rb +0 -7
- data/spec/conceptql/operators/race_spec.rb +0 -21
- data/spec/conceptql/operators/rxnorm_spec.rb +0 -29
- data/spec/conceptql/operators/snomed_spec.rb +0 -29
- data/spec/conceptql/operators/source_vocabulary_operator_spec.rb +0 -35
- data/spec/conceptql/operators/standard_vocabulary_operator_spec.rb +0 -35
- data/spec/conceptql/operators/started_by_spec.rb +0 -22
- data/spec/conceptql/operators/temporal_operator_spec.rb +0 -51
- data/spec/conceptql/operators/time_window_spec.rb +0 -77
- data/spec/conceptql/operators/union_spec.rb +0 -21
- data/spec/conceptql/operators/visit_occurrence_spec.rb +0 -10
- data/spec/conceptql/query_spec.rb +0 -22
- data/spec/conceptql/tree_spec.rb +0 -50
- data/spec/doubles/stream_for_casting_double.rb +0 -9
- data/spec/doubles/stream_for_occurrence_double.rb +0 -25
- data/spec/doubles/stream_for_temporal_double.rb +0 -6
- data/spec/spec_helper.rb +0 -102
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "logger"
|
|
5
|
+
require_relative "lexicon"
|
|
6
|
+
|
|
7
|
+
module ConceptQL
|
|
8
|
+
class Database
|
|
9
|
+
attr_reader :db, :opts
|
|
10
|
+
|
|
11
|
+
@lexicon_mutex = Mutex.new
|
|
12
|
+
|
|
13
|
+
EXTENSIONS = %i[
|
|
14
|
+
date_arithmetic
|
|
15
|
+
error_sql
|
|
16
|
+
make_readyable
|
|
17
|
+
null_dataset
|
|
18
|
+
pg_ctas_explain
|
|
19
|
+
pg_vacuum_table
|
|
20
|
+
smart_select_remove
|
|
21
|
+
sql_comments
|
|
22
|
+
usable
|
|
23
|
+
].freeze
|
|
24
|
+
|
|
25
|
+
def initialize(db, opts = {})
|
|
26
|
+
@db = db
|
|
27
|
+
# Symbolize all keys and values
|
|
28
|
+
@opts = ConceptQL::Utils.rekey(opts, rekey_values: true)
|
|
29
|
+
|
|
30
|
+
@opts[:data_model] ||= (ENV["CONCEPTQL_DATA_MODEL"] || ConceptQL::DEFAULT_DATA_MODEL).to_sym
|
|
31
|
+
|
|
32
|
+
self.class.db_extensions(db, @opts[:data_model], @opts[:use_cold_col])
|
|
33
|
+
db_type = db ? db.database_type.to_sym : :postgres
|
|
34
|
+
|
|
35
|
+
@lexicon = opts[:lexicon]
|
|
36
|
+
if @lexicon && @opts[:cache_lookups]
|
|
37
|
+
raise ArgumentError, "cache_lookups needs the Database to build its own Lexicon; do not pass lexicon:"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
@opts[:database_type] ||= (ENV["CONCEPTQL_DATABASE_TYPE"] || db_type).to_sym
|
|
41
|
+
@opts[:scope_opts] = {
|
|
42
|
+
force_temp_tables: opts.fetch(:force_temp_tables, ENV["CONCEPTQL_FORCE_TEMP_TABLES"] == "true"),
|
|
43
|
+
scratch_database: opts.fetch(:scratch_database, ENV["DOCKER_SCRATCH_DATABASE"])
|
|
44
|
+
}.merge(opts[:scope_opts] || {})
|
|
45
|
+
@opts[:scope_opts][:lexicon] = lexicon
|
|
46
|
+
|
|
47
|
+
@shared_utilization_tables = {}
|
|
48
|
+
@shared_utilization_mutex = Mutex.new
|
|
49
|
+
@source_tables = {} if @opts[:cache_lookups]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def query(statement, opts = {})
|
|
53
|
+
opts[:scope_opts] = (@opts[:scope_opts] || {}).merge(opts.delete(:scope_opts) || {})
|
|
54
|
+
if @opts[:share_utilization] && opts[:scope_opts][:table_prefix].to_s.empty?
|
|
55
|
+
raise ArgumentError, "share_utilization needs a table_prefix in scope_opts, to name its tables"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
Query.new(self, ConceptQL::Utils.rekey(statement), @opts.merge(opts))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def data_model
|
|
62
|
+
@data_model ||= DataModel.get(opts[:data_model], nodifier: nodifier)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def nodifier
|
|
66
|
+
@nodifier ||= Nodifier.new(self)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def base_data_model
|
|
70
|
+
data_model.base
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
class << self
|
|
74
|
+
def db_extensions(db, data_model, use_cold_col)
|
|
75
|
+
return unless db
|
|
76
|
+
|
|
77
|
+
EXTENSIONS.each do |extension|
|
|
78
|
+
db.extension extension
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
use_cold_col = db.is_a?(Sequel::Mock::Database) if use_cold_col.nil?
|
|
82
|
+
return unless use_cold_col
|
|
83
|
+
|
|
84
|
+
db.extension(:cold_col)
|
|
85
|
+
db.load_schema(ConceptQL.schemas_dir / "#{data_model}.yml")
|
|
86
|
+
db.load_schema(ConceptQL.schemas_dir / "ohdsi_vocabs.yml")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def lexicon_db
|
|
90
|
+
@lexicon_mutex.synchronize do
|
|
91
|
+
@lexicon_db = make_lexicon_db unless defined?(@lexicon_db)
|
|
92
|
+
end
|
|
93
|
+
@lexicon_db
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# The LEXICON_URL database, or nil when it is unset. Lexicon tries the
|
|
97
|
+
# data db first and, when neither has the vocabulary tables, falls back
|
|
98
|
+
# to LexiconNoDB, so no database (and no sqlite3 gem) is needed here.
|
|
99
|
+
def make_lexicon_db
|
|
100
|
+
return unless ENV["LEXICON_URL"]
|
|
101
|
+
|
|
102
|
+
db_opts = {}
|
|
103
|
+
if ENV["CONCEPTQL_LOG_LEXICON"]
|
|
104
|
+
log_path = Pathname.new("log") / "conceptql_lexicon.log"
|
|
105
|
+
log_path.dirname.mkpath
|
|
106
|
+
db_opts[:logger] = Logger.new(log_path)
|
|
107
|
+
end
|
|
108
|
+
lexicon_db = Sequel.connect(ENV["LEXICON_URL"], db_opts)
|
|
109
|
+
lexicon_db.extension(:date_arithmetic)
|
|
110
|
+
lexicon_db
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Whether a data model's source table exists in db. With cache_lookups on,
|
|
115
|
+
# a table that exists is remembered per db and table for the life of this
|
|
116
|
+
# Database, as the Lexicon keeps its lookups. A missing table is checked
|
|
117
|
+
# again on every call: Sequel's table_exists? also answers false on a
|
|
118
|
+
# database error, and one transient error must not turn off the database
|
|
119
|
+
# checks for the whole export. Only the data model's fixed source tables
|
|
120
|
+
# come here, never a table that conceptql creates during a run.
|
|
121
|
+
def source_table_exists?(db, table)
|
|
122
|
+
return db.table_exists?(table) unless @source_tables
|
|
123
|
+
|
|
124
|
+
key = [ db, table ]
|
|
125
|
+
@source_tables.fetch(key) { db.table_exists?(table) && (@source_tables[key] = true) }
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def lexicon
|
|
129
|
+
@lexicon ||= Lexicon.new(self.class.lexicon_db, db, cache: @opts[:cache_lookups])
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# This Database's operators for a data model, as a frozen name => class
|
|
133
|
+
# Hash: a vocabulary operator for every vocabulary in config/vocabularies.csv
|
|
134
|
+
# (plus the custom file) and in this Database's lexicon, with the built-in
|
|
135
|
+
# operators merged over them. A built-in wins a name clash (gender, race and
|
|
136
|
+
# ethnicity are also vocabularies). Built lazily, once per data model.
|
|
137
|
+
#
|
|
138
|
+
# The data model is an argument because a Nodifier can be given one that
|
|
139
|
+
# differs from this Database's.
|
|
140
|
+
def operators(data_model = opts[:data_model])
|
|
141
|
+
data_model = data_model.to_sym
|
|
142
|
+
@operators ||= {}
|
|
143
|
+
@operators[data_model] ||= begin
|
|
144
|
+
vocabulary_operators = Vocabularies::DynamicVocabularies.new(lexicon).operators(data_model)
|
|
145
|
+
vocabulary_operators.merge(Operators.static_operators.fetch(data_model)).freeze
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Builds the table that the name_concepts_table option names. It holds
|
|
150
|
+
# the concept rows whose ids appear in the data columns that the
|
|
151
|
+
# display-name joins read, so those joins hash a small table instead of
|
|
152
|
+
# the whole concept table. The caller drops it.
|
|
153
|
+
def create_name_concepts_table
|
|
154
|
+
table = opts[:name_concepts_table]
|
|
155
|
+
raise ArgumentError, "create_name_concepts_table needs the name_concepts_table option" unless table
|
|
156
|
+
|
|
157
|
+
ids = data_model.name_concept_columns.flat_map do |source, columns|
|
|
158
|
+
next [] unless db.table_exists?(source)
|
|
159
|
+
|
|
160
|
+
(columns & db[source].columns).map { |column| db[source].exclude(column => nil).select(Sequel.as(column, :id)) }
|
|
161
|
+
end
|
|
162
|
+
raise ArgumentError, "no data columns to read for data model #{opts[:data_model]}" if ids.empty?
|
|
163
|
+
|
|
164
|
+
db.create_table(table, as: data_model.concepts_table(db).where(id: ids.reduce { |all, one| all.union(one) }))
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# The share_utilization option: the name of the table that holds a
|
|
168
|
+
# Utilizable operator's list (its select_it(query(db)), passed as key).
|
|
169
|
+
# The key's SQL contains the window table and adjustments, so two
|
|
170
|
+
# different windows get two tables. The name is a Symbol, because
|
|
171
|
+
# sequel-duckdb's table_exists? reads a Sequel::SQL::Identifier wrong.
|
|
172
|
+
def shared_utilization_table(key, scope)
|
|
173
|
+
name = :"#{scope.opts[:table_prefix]}_util_#{Digest::MD5.hexdigest(key.sql)[0, 16]}"
|
|
174
|
+
scope.scratch_database.to_s.empty? ? name : Sequel.qualify(scope.scratch_database.to_sym, name)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# The sql_statements entry that creates a shared_utilization_table, or nil
|
|
178
|
+
# when the table exists. The caller runs the entries in order. Nothing is
|
|
179
|
+
# claimed: every call lists a table until it exists.
|
|
180
|
+
def shared_utilization_statement(name, key)
|
|
181
|
+
return if db.table_exists?(name)
|
|
182
|
+
|
|
183
|
+
@shared_utilization_mutex.synchronize { @shared_utilization_tables[key.sql] = name }
|
|
184
|
+
[ name, key.sql ]
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Creates a shared_utilization_table that does not exist yet, when a
|
|
188
|
+
# query first reads rows through conceptql. Another thread that reads the
|
|
189
|
+
# same table waits here and then reads the table the first thread created.
|
|
190
|
+
def create_shared_utilization_table(name, key, rdbms, scope)
|
|
191
|
+
@shared_utilization_mutex.synchronize do
|
|
192
|
+
next if db.table_exists?(name)
|
|
193
|
+
|
|
194
|
+
db.create_table(name, rdbms.create_options(scope, key).merge(as: key))
|
|
195
|
+
rdbms.post_create(db, name)
|
|
196
|
+
@shared_utilization_tables[key.sql] = name
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# The shared tables that conceptql listed in sql_statements or created.
|
|
201
|
+
# conceptql never drops them: the caller does.
|
|
202
|
+
def shared_tables
|
|
203
|
+
@shared_utilization_mutex.synchronize { @shared_utilization_tables.values }
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def database_type
|
|
207
|
+
@opts[:database_type]
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def type_for(column_name)
|
|
211
|
+
Scope::COLUMN_TYPES.fetch(column_name)
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
@@ -1,43 +1,91 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../conceptql"
|
|
2
4
|
|
|
3
5
|
module ConceptQL
|
|
4
6
|
# Used to translate a string of terse date adjustments into a set of adjustments that are compatible with most RDBMSs
|
|
5
7
|
class DateAdjuster
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
VALID_INPUT = /\A#{Regexp.union([ /START/i, /END/i, /\d{4}-\d{2}-\d{2}/, /[ers]*([-+]?[\ddwmy]+)+/i,
|
|
9
|
+
/\s*/ ])}\z/.freeze
|
|
10
|
+
|
|
11
|
+
attr_reader :op, :str, :manipulator
|
|
12
|
+
|
|
13
|
+
def initialize(op, str, opts = {})
|
|
14
|
+
str = str.strftime("%Y-%m-%d") if str.respond_to?(:strftime)
|
|
15
|
+
raise "Invalid adjustment string: #{str.pretty_inspect}" unless str.nil? || str =~ VALID_INPUT
|
|
16
|
+
|
|
17
|
+
@op = op
|
|
18
|
+
@str = str || ""
|
|
19
|
+
@manipulator = opts[:manipulator] || Sequel
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def adjust(column, reverse = false)
|
|
23
|
+
return Sequel.expr(:end_date) if str.downcase == "end"
|
|
24
|
+
return Sequel.expr(:start_date) if str.downcase == "start"
|
|
25
|
+
return op.rdbms.cast_date(Date.parse(str).strftime("%Y-%m-%d")) if str =~ /^\d{4}-\d{2}-\d{2}$/
|
|
26
|
+
|
|
27
|
+
origin_column = column
|
|
28
|
+
|
|
29
|
+
chrs = str.chars
|
|
30
|
+
while (chr = chrs.shift) =~ /[res]/i
|
|
31
|
+
if chr.downcase == "r"
|
|
32
|
+
reverse = true
|
|
33
|
+
else
|
|
34
|
+
origin_column = chr.downcase == "e" ? :end_date : :start_date
|
|
35
|
+
origin_column = Sequel.qualify(column.table.to_sym, origin_column) if column.respond_to?(:table)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
adjusted_date = adjustments(reverse).inject(Sequel.expr(origin_column)) do |sql, (units, quantity)|
|
|
40
|
+
# Turns out weeks aren't supported in Sequel, so we'll just multiply
|
|
41
|
+
# the number of weeks by 7 and adjust the date by that number of days
|
|
42
|
+
if units == :weeks
|
|
43
|
+
units = :days
|
|
44
|
+
quantity *= 7
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
if quantity.positive?
|
|
48
|
+
manipulator.date_add(sql, units => quantity)
|
|
49
|
+
else
|
|
50
|
+
manipulator.date_sub(sql, units => quantity.abs)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
op.rdbms.cast_date(adjusted_date)
|
|
9
54
|
end
|
|
10
55
|
|
|
11
56
|
# Returns an array of strings that represent date modifiers
|
|
12
|
-
def adjustments
|
|
13
|
-
@adjustments ||= parse(str)
|
|
57
|
+
def adjustments(reverse = false)
|
|
58
|
+
@adjustments ||= parse(str, reverse)
|
|
14
59
|
end
|
|
15
60
|
|
|
16
61
|
private
|
|
62
|
+
|
|
17
63
|
def lookup
|
|
18
64
|
{
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
65
|
+
"y" => :years,
|
|
66
|
+
"m" => :months,
|
|
67
|
+
"w" => :weeks,
|
|
68
|
+
"d" => :days
|
|
22
69
|
}
|
|
23
70
|
end
|
|
24
71
|
|
|
25
|
-
def parse(str)
|
|
26
|
-
ConceptQL.logger.debug(str)
|
|
72
|
+
def parse(str, reverse)
|
|
27
73
|
return [] if str.nil? || str.empty?
|
|
28
|
-
return [[lookup[
|
|
29
|
-
|
|
74
|
+
return [ [ lookup["d"], str.to_i ] ] if str.match(/^[-+]?\d+$/)
|
|
75
|
+
|
|
76
|
+
str.downcase.scan(/([-+]?\d*[dwmy])/).map do |adjustment|
|
|
30
77
|
adjustment = adjustment.first
|
|
78
|
+
|
|
31
79
|
quantity = 1
|
|
32
80
|
if adjustment.match(/\d/)
|
|
33
81
|
quantity = adjustment.to_i
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
quantity = -1
|
|
37
|
-
end
|
|
82
|
+
elsif adjustment.chars.first == "-"
|
|
83
|
+
quantity = -1
|
|
38
84
|
end
|
|
85
|
+
quantity *= -1 if reverse
|
|
86
|
+
|
|
39
87
|
unit = lookup[adjustment.chars.last]
|
|
40
|
-
[unit, quantity]
|
|
88
|
+
[ unit, quantity ]
|
|
41
89
|
end
|
|
42
90
|
end
|
|
43
91
|
end
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "active_support/core_ext/hash/indifferent_access"
|
|
5
|
+
|
|
6
|
+
module ConceptQL
|
|
7
|
+
# Converts ConceptQL statements into trees a diagram renderer can draw.
|
|
8
|
+
#
|
|
9
|
+
# Two shapes are produced:
|
|
10
|
+
#
|
|
11
|
+
# * RenderTree -- the `conceptql-diagram/v1` format: one node per operator,
|
|
12
|
+
# carrying the operator's metadata (base type, human name) and what
|
|
13
|
+
# conceptql's own annotation worked out for it (output types, errors,
|
|
14
|
+
# warnings and, optionally, per-domain counts).
|
|
15
|
+
# * JigsawTree -- the Jigsaw diagram editor's tree JSON (name, values,
|
|
16
|
+
# parameters, children), as produced by the editor's ConceptqlAdapter.
|
|
17
|
+
module Diagram
|
|
18
|
+
FORMAT = "conceptql-diagram/v1"
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
# The full `conceptql-diagram/v1` document for one or more statements.
|
|
22
|
+
#
|
|
23
|
+
# Without `counts: true` no claims database is needed: pass a cdb built
|
|
24
|
+
# on a nil db (the default).
|
|
25
|
+
def render(statements, cdb: default_cdb, counts: false)
|
|
26
|
+
{
|
|
27
|
+
format: FORMAT,
|
|
28
|
+
statements: RenderTree.new(cdb, counts: counts).build_all(statements)
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def default_cdb(data_model = nil)
|
|
33
|
+
opts = {}
|
|
34
|
+
opts[:data_model] = data_model.to_sym if data_model
|
|
35
|
+
ConceptQL::Database.new(nil, opts)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# A statement file may hold a single statement or a list of them.
|
|
39
|
+
# Mirrors JDE's ConceptqlAdapter#nested.
|
|
40
|
+
def statement_list(statements)
|
|
41
|
+
statements = JSON.parse(statements) if statements.is_a?(String)
|
|
42
|
+
return [] if statements == []
|
|
43
|
+
return statements if statements.first.is_a?(Array)
|
|
44
|
+
|
|
45
|
+
[ statements ]
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
class RenderTree
|
|
50
|
+
# Operator options that are structure or bookkeeping, not parameters
|
|
51
|
+
# a diagram should show.
|
|
52
|
+
STRUCTURAL_OPTIONS = %i[annotation name left right id].freeze
|
|
53
|
+
|
|
54
|
+
attr_reader :cdb
|
|
55
|
+
|
|
56
|
+
def initialize(cdb, counts: false)
|
|
57
|
+
@cdb = cdb
|
|
58
|
+
@counts = counts
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def counts?
|
|
62
|
+
@counts
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def build_all(statements)
|
|
66
|
+
Diagram.statement_list(statements).map { |statement| build(statement) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def build(statement)
|
|
70
|
+
node(cdb.query(statement).annotate(skip_counts: !counts?))
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def node(annotated)
|
|
76
|
+
name, *rest = annotated
|
|
77
|
+
hashes, rest = rest.partition { |v| v.is_a?(Hash) }
|
|
78
|
+
opts = hashes.reduce({}, :merge)
|
|
79
|
+
upstreams, values = rest.partition { |v| v.is_a?(Array) }
|
|
80
|
+
annotation = opts[:annotation] || {}
|
|
81
|
+
|
|
82
|
+
children = upstreams + opts.values_at(:left, :right).compact
|
|
83
|
+
node = {
|
|
84
|
+
name: name.to_s,
|
|
85
|
+
base: metadata_for(name)[:basic_type]&.to_s,
|
|
86
|
+
humanName: opts[:name] || metadata_for(name)[:preferred_name] || name.to_s
|
|
87
|
+
}
|
|
88
|
+
node[:vocabularyId] = metadata_for(name)[:preferred_name] if vocabulary?(name)
|
|
89
|
+
node.merge!(
|
|
90
|
+
outputTypes: output_types(annotation),
|
|
91
|
+
parameters: opts.except(*STRUCTURAL_OPTIONS),
|
|
92
|
+
values: values,
|
|
93
|
+
children: children.map { |child| node(child) }
|
|
94
|
+
)
|
|
95
|
+
node[:counts] = counts(annotation) if counts?
|
|
96
|
+
node[:errors] = annotation[:errors] if annotation[:errors].present?
|
|
97
|
+
node[:warnings] = annotation[:warnings] if annotation[:warnings].present?
|
|
98
|
+
node
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# conceptql fills annotation[:counts] from Operator#domains, so its keys
|
|
102
|
+
# are the node's output types. Vocabulary metadata nests them
|
|
103
|
+
# ([["condition_occurrence"]]) where cast operators do not ([:person]),
|
|
104
|
+
# so flatten defensively. Sorted so that a counted run (whose keys arrive
|
|
105
|
+
# in database order) matches an uncounted one.
|
|
106
|
+
def output_types(annotation)
|
|
107
|
+
(annotation[:counts] || {}).keys.flatten.map(&:to_s).uniq.sort
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def counts(annotation)
|
|
111
|
+
(annotation[:counts] || {})
|
|
112
|
+
.sort_by { |domain, _| domain.to_s }
|
|
113
|
+
.to_h { |domain, c| [ domain.to_s, { rows: Integer(c[:rows] || 0), n: Integer(c[:n] || 0) } ] }
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def metadata_for(name)
|
|
117
|
+
operators_metadata[canonical_name(name)] || {}
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def operators_metadata
|
|
121
|
+
@operators_metadata ||= ConceptQL.metadata(cdb)[:operators]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# A single-vocabulary operator (icd9, loinc, ...). Its metadata
|
|
125
|
+
# preferred_name is the vocabulary's ID (Entry#preferred_name:
|
|
126
|
+
# omopv5_id || id, e.g. "ICD9CM", "LOINC"), which a renderer can show
|
|
127
|
+
# when the humanName (the vocabulary's short name) is too long.
|
|
128
|
+
# Multi-vocabulary operators ("CPT or HCPCS") span several IDs, so they
|
|
129
|
+
# are not included.
|
|
130
|
+
def vocabulary?(name)
|
|
131
|
+
klass = operator_classes[canonical_name(name)]
|
|
132
|
+
!klass.nil? && klass <= ConceptQL::Operators::Vocabulary
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def operator_classes
|
|
136
|
+
@operator_classes ||= cdb.operators(cdb.opts[:data_model])
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def canonical_name(name)
|
|
140
|
+
name = name.to_s
|
|
141
|
+
operators_metadata.key?(name) ? name : aliases[name]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def aliases
|
|
145
|
+
@aliases ||= operators_metadata.each_with_object({}) do |(name, md), h|
|
|
146
|
+
Array(md[:aliases]).each { |a| h[a.to_s] = name }
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Port of the Jigsaw diagram editor's ConceptqlAdapter#to_jigsaw_json.
|
|
152
|
+
# The only change is where binary (filter) operators are looked up: the
|
|
153
|
+
# editor asked its Rails-wide cdb, this takes one (db-less by default).
|
|
154
|
+
class JigsawTree
|
|
155
|
+
BINARY_PARAMETERS = %w[left right].freeze
|
|
156
|
+
IGNORED_PARAMETERS = %w[id].freeze
|
|
157
|
+
|
|
158
|
+
class Node
|
|
159
|
+
def initialize(operator, binary)
|
|
160
|
+
@operator = operator
|
|
161
|
+
@binary = binary
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def name
|
|
165
|
+
operator[0]
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def values
|
|
169
|
+
operator[1..].select { |v| v.is_a?(String) || v.is_a?(Integer) }
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def parameters
|
|
173
|
+
return unless operator[-1].is_a?(Hash)
|
|
174
|
+
|
|
175
|
+
operator[-1]
|
|
176
|
+
.reject { |k, _v| IGNORED_PARAMETERS.include?(k.to_s) }
|
|
177
|
+
.with_indifferent_access
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def children
|
|
181
|
+
operator[1..right_bound].grep(Array)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def binary?
|
|
185
|
+
@binary.include?(name)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
private
|
|
189
|
+
|
|
190
|
+
attr_reader :operator
|
|
191
|
+
|
|
192
|
+
def right_bound
|
|
193
|
+
parameters.present? ? -2 : -1
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def initialize(statements, cdb: Diagram.default_cdb)
|
|
198
|
+
@statements = Diagram.statement_list(statements)
|
|
199
|
+
@cdb = cdb
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def to_jigsaw_json
|
|
203
|
+
{ name: "root", children: statements.map { |s| build_node(s) } }
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
private
|
|
207
|
+
|
|
208
|
+
attr_reader :statements, :cdb
|
|
209
|
+
|
|
210
|
+
def binary
|
|
211
|
+
@binary ||= ConceptQL.metadata(cdb)[:operators].select { |_k, v| v[:basic_type] == :filter }.keys
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def build_node(operator)
|
|
215
|
+
node = Node.new(operator, binary)
|
|
216
|
+
{
|
|
217
|
+
name: node.name,
|
|
218
|
+
values: node.values,
|
|
219
|
+
parameters: build_parameters(node),
|
|
220
|
+
children: build_children(node)
|
|
221
|
+
}.reject { |_k, v| v.blank? }
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def build_parameters(node)
|
|
225
|
+
return unless node.parameters
|
|
226
|
+
|
|
227
|
+
node.parameters.reject { |p| BINARY_PARAMETERS.include?(p.to_s) }
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def build_children(node)
|
|
231
|
+
kids = node.children.map { |child| build_node(child) }
|
|
232
|
+
kids += build_binary_children(node) if node.binary?
|
|
233
|
+
kids
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def build_binary_children(node)
|
|
237
|
+
return [] unless node.parameters
|
|
238
|
+
|
|
239
|
+
BINARY_PARAMETERS.filter_map { |p| build_node(node.parameters[p]) if node.parameters[p] }
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lexicon_strategy"
|
|
4
|
+
|
|
5
|
+
module ConceptQL
|
|
6
|
+
class LexiconGDM < LexiconStrategy
|
|
7
|
+
class << self
|
|
8
|
+
def db_has_all_vocabulary_tables?(db)
|
|
9
|
+
%i[ancestors concepts mappings vocabularies].all? { |t| db.table_exists?(t) }
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def vocabularies_query
|
|
14
|
+
db[:vocabularies]
|
|
15
|
+
.select(Sequel[:id].as(:id),
|
|
16
|
+
Sequel[:id].as(:omopv5_vocabulary_id),
|
|
17
|
+
Sequel[:vocabulary_name].as(:vocabulary_short_name),
|
|
18
|
+
Sequel[:vocabulary_name].as(:vocabulary_full_name),
|
|
19
|
+
Sequel[:domain],
|
|
20
|
+
Sequel.expr(1).as(:from_lexicon))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def vocabularies
|
|
24
|
+
vocabularies_query.all
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def is_a_relationships(_data_db)
|
|
28
|
+
db[:mappings].where { Sequel.function(:lower, :relationship_id) =~ "is_a" }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def ancestors_table(_data_db)
|
|
32
|
+
db[:ancestors]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def concepts_table(_data_db, _some_schema = nil)
|
|
36
|
+
db[:concepts]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def table_is_missing?(_data_db)
|
|
40
|
+
!db.table_exists?(:concepts)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def strategy
|
|
44
|
+
:gdm
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lexicon_strategy"
|
|
4
|
+
|
|
5
|
+
module ConceptQL
|
|
6
|
+
# The strategy used when neither the dataset db nor the lexicon db has
|
|
7
|
+
# vocabulary tables (for instance a db-less ConceptQL::Database, as the
|
|
8
|
+
# diagram renderer uses). Its db is a Sequel mock, so every lookup returns
|
|
9
|
+
# no rows, but SQL that joins or selects from the vocabulary tables can
|
|
10
|
+
# still be BUILT.
|
|
11
|
+
#
|
|
12
|
+
# It must answer the same table methods as the other strategies, because
|
|
13
|
+
# LexiconStrategy's lookups (concepts, descendants_of,
|
|
14
|
+
# related_concept_ids, ...) are written against them. The tables are named
|
|
15
|
+
# in the GDM layout (concepts / ancestors / mappings), matching
|
|
16
|
+
# #vocabularies_query, which already reads GDM's `vocabularies` table, and
|
|
17
|
+
# the columns that LexiconStrategy's queries use (id, concept_code,
|
|
18
|
+
# vocabulary_id, concept_text; ancestor_id / descendant_id;
|
|
19
|
+
# concept_1_id / concept_2_id).
|
|
20
|
+
class LexiconNoDB < LexiconStrategy
|
|
21
|
+
def vocabularies_query
|
|
22
|
+
db[:vocabularies]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def vocabularies
|
|
26
|
+
[]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def is_a_relationships(_data_db)
|
|
30
|
+
db[:mappings].where { Sequel.function(:lower, :relationship_id) =~ "is_a" }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def ancestors_table(_data_db)
|
|
34
|
+
db[:ancestors]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def concepts_table(_data_db, _some_schema = nil)
|
|
38
|
+
db[:concepts]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def table_is_missing?(_db)
|
|
42
|
+
true
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def strategy
|
|
46
|
+
:no_db
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|