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,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "generic"
|
|
4
|
+
|
|
5
|
+
module ConceptQL
|
|
6
|
+
module Rdbms
|
|
7
|
+
class Postgres < Generic
|
|
8
|
+
# Returns from - to (see Generic#datediff).
|
|
9
|
+
def datediff(from, to)
|
|
10
|
+
Sequel.extract(:days, Sequel.cast(from, Time) - Sequel.cast(to, Time))
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def post_create(db, table_name)
|
|
14
|
+
db.vacuum_table(table_name, analyze: true) if analyze_temp_tables?
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def preferred_formatter
|
|
18
|
+
SqlFormatters::POSTGRES
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# DISTINCT ON is Postgres-only. The other adapters inherit
|
|
22
|
+
# Generic#primary_concepts, which uses ROW_NUMBER() instead.
|
|
23
|
+
def primary_concepts(db, all_primary_ids)
|
|
24
|
+
db[Sequel[:clinical_codes].as(:pcc)]
|
|
25
|
+
.where(provenance_concept_id: all_primary_ids)
|
|
26
|
+
.select(
|
|
27
|
+
Sequel[:pcc][:collection_id].as(:collection_id),
|
|
28
|
+
Sequel[:pcc][:clinical_code_concept_id].as(:concept_id),
|
|
29
|
+
Sequel[:pcc][:clinical_code_source_value].as(:concept_code),
|
|
30
|
+
Sequel[:pcc][:clinical_code_vocabulary_id].as(:vocabulary_id),
|
|
31
|
+
Sequel[true].as(:is_primary)
|
|
32
|
+
)
|
|
33
|
+
.order(Sequel[:pcc][:collection_id], Sequel[:pcc][:clinical_code_concept_id])
|
|
34
|
+
.from_self
|
|
35
|
+
.distinct(:collection_id)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def analyze_temp_tables?
|
|
39
|
+
ENV["CONCEPTQL_PG_ANALYZE_TEMP_TABLES"] == "true"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def explain_temp_tables?
|
|
43
|
+
ENV["CONCEPTQL_PG_EXPLAIN_TEMP_TABLES"] == "true"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def supports_materialized?
|
|
47
|
+
true
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "generic"
|
|
4
|
+
|
|
5
|
+
module ConceptQL
|
|
6
|
+
module Rdbms
|
|
7
|
+
class Spark < Generic
|
|
8
|
+
def days_between(from_column, to_column)
|
|
9
|
+
Sequel.function(:datediff, cast_date(to_column), cast_date(from_column))
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# <=> is Spark's NULL-safe equality operator (see Generic#null_safe_equal).
|
|
13
|
+
def null_safe_equal(lhs, rhs)
|
|
14
|
+
Sequel.lit("(? <=> ?)", lhs, rhs)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def preferred_formatter
|
|
18
|
+
SqlFormatters::PG_FORMAT
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "rdbms/postgres"
|
|
4
|
+
require_relative "rdbms/spark"
|
|
5
|
+
|
|
6
|
+
module ConceptQL
|
|
7
|
+
module Rdbms
|
|
8
|
+
def self.generate(database_type)
|
|
9
|
+
case database_type.to_sym
|
|
10
|
+
when :postgres
|
|
11
|
+
ConceptQL::Rdbms::Postgres.new
|
|
12
|
+
when :spark
|
|
13
|
+
ConceptQL::Rdbms::Spark.new
|
|
14
|
+
when :duckdb
|
|
15
|
+
require_relative "rdbms/duckdb"
|
|
16
|
+
ConceptQL::Rdbms::DuckDB.new
|
|
17
|
+
else
|
|
18
|
+
raise "Unknown database_type -- '#{database_type}'"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
data/lib/conceptql/scope.rb
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "English"
|
|
4
|
+
require_relative "window"
|
|
5
|
+
require "securerandom"
|
|
6
|
+
require "sequel"
|
|
7
|
+
|
|
2
8
|
module ConceptQL
|
|
3
|
-
# Scope coordinates the creation of any
|
|
9
|
+
# Scope coordinates the creation of any common table expressions that might
|
|
4
10
|
# be used when a Recall operator is present in the statement.
|
|
5
11
|
#
|
|
6
12
|
# Any time an operator is given a label, it becomes a candidate
|
|
@@ -8,62 +14,506 @@ module ConceptQL
|
|
|
8
14
|
# somewhere else in the statement.
|
|
9
15
|
#
|
|
10
16
|
# Scope keeps track of all labeled operators and provides an
|
|
11
|
-
# API for Recall operators to fetch the results/
|
|
17
|
+
# API for Recall operators to fetch the results/domains from
|
|
12
18
|
# labeled operators.
|
|
13
19
|
class Scope
|
|
20
|
+
# Raised when the extracted CTEs read each other in a cycle, so no order
|
|
21
|
+
# puts every CTE after the CTEs it reads.
|
|
22
|
+
class CteCycleError < StandardError
|
|
23
|
+
def initialize(names)
|
|
24
|
+
super("These CTEs read each other in a cycle, so they have no valid order: #{names.join(', ')}")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
DEFAULT_COLUMN_FAMILY = "default"
|
|
29
|
+
|
|
30
|
+
DEFAULT_COLUMNS = {
|
|
31
|
+
person_id: :Bigint,
|
|
32
|
+
criterion_id: :Bigint,
|
|
33
|
+
criterion_table: :String,
|
|
34
|
+
criterion_domain: :String,
|
|
35
|
+
start_date: :Date,
|
|
36
|
+
end_date: :Date,
|
|
37
|
+
source_value: :String,
|
|
38
|
+
source_vocabulary_id: :String,
|
|
39
|
+
label: :String,
|
|
40
|
+
column_family: :String
|
|
41
|
+
}.freeze
|
|
42
|
+
|
|
43
|
+
ADDITIONAL_COLUMNS = {
|
|
44
|
+
value_as_number: :Float,
|
|
45
|
+
value_as_string: :String,
|
|
46
|
+
value_as_concept_id: :Bigint,
|
|
47
|
+
value_as_concept: :String,
|
|
48
|
+
unit_concept_id: :Bigint,
|
|
49
|
+
unit_concept: :String,
|
|
50
|
+
context_id: :Bigint,
|
|
51
|
+
visit_occurrence_id: :Bigint,
|
|
52
|
+
file_provenance_type: :Bigint,
|
|
53
|
+
code_provenance_type: :Bigint,
|
|
54
|
+
provider_id: :Bigint,
|
|
55
|
+
context_specialty_concept_id: :Bigint,
|
|
56
|
+
practitioner_specialty_concept_id: :Bigint,
|
|
57
|
+
role_type_concept_id: :Bigint,
|
|
58
|
+
visit_source_concept_id: :Bigint,
|
|
59
|
+
range_low: :Float,
|
|
60
|
+
range_high: :Float,
|
|
61
|
+
drug_name: :String,
|
|
62
|
+
drug_amount: :Float,
|
|
63
|
+
drug_amount_units: :String,
|
|
64
|
+
drug_strength_source_value: :String,
|
|
65
|
+
drug_days_supply: :Bigint,
|
|
66
|
+
drug_quantity: :Float,
|
|
67
|
+
admission_date: :Date,
|
|
68
|
+
discharge_date: :Date,
|
|
69
|
+
length_of_stay: :Bigint,
|
|
70
|
+
admission_source_value: :String,
|
|
71
|
+
admission_source_description: :String,
|
|
72
|
+
discharge_location_source_value: :String,
|
|
73
|
+
discharge_location_source_description: :String,
|
|
74
|
+
primary_concept_id: :Bigint,
|
|
75
|
+
primary_diagnosis_code: :String,
|
|
76
|
+
primary_diagnosis_vocabulary: :String,
|
|
77
|
+
uuid: :String,
|
|
78
|
+
window_id: :Bigint
|
|
79
|
+
}.freeze
|
|
80
|
+
|
|
81
|
+
COLUMN_TYPES = DEFAULT_COLUMNS.merge(ADDITIONAL_COLUMNS).freeze
|
|
82
|
+
|
|
83
|
+
# Marks the WITH entry of a list that the Database's share_utilization
|
|
84
|
+
# option keeps in a shared table (Behaviors::Utilizable#evaluate).
|
|
85
|
+
SHARED_TABLE = :conceptql_shared_table
|
|
86
|
+
|
|
14
87
|
attr_accessor :person_ids
|
|
15
|
-
|
|
16
|
-
|
|
88
|
+
|
|
89
|
+
attr_reader :known_operators, :recall_stack, :recall_dependencies, :annotation, :opts, :query_columns
|
|
90
|
+
|
|
91
|
+
def initialize(opts = {})
|
|
17
92
|
@known_operators = {}
|
|
18
|
-
@
|
|
93
|
+
@recall_dependencies = {}
|
|
94
|
+
@recall_stack = []
|
|
95
|
+
@label_cte_names = {}
|
|
96
|
+
@annotation = {}
|
|
97
|
+
@opts = opts.dup
|
|
98
|
+
@annotation[:errors] = @errors = {}
|
|
99
|
+
@annotation[:warnings] = @warnings = {}
|
|
100
|
+
@annotation[:counts] = @counts = {}
|
|
101
|
+
@query_columns = DEFAULT_COLUMNS.keys
|
|
102
|
+
@query_columns << :window_id if opts.dig(:window_opts, :window_table)
|
|
103
|
+
|
|
104
|
+
@i = 0
|
|
105
|
+
@mutex = Mutex.new
|
|
106
|
+
@cte_name_next = -> { @mutex.synchronize { @i += 1 } }
|
|
107
|
+
|
|
108
|
+
return unless force_temp_tables? && scratch_database.to_s.empty?
|
|
109
|
+
|
|
110
|
+
raise ArgumentError,
|
|
111
|
+
"You must set the DOCKER_SCRATCH_DATABASE environment variable to the name of the scratch database if using the CONCEPTQL_FORCE_TEMP_TABLES environment variable"
|
|
19
112
|
end
|
|
20
113
|
|
|
21
|
-
def
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
114
|
+
def add_errors(key, errors)
|
|
115
|
+
@errors[key] = errors
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def add_warnings(key, errors)
|
|
119
|
+
@warnings[key] = errors
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def add_counts(key, domain, counts)
|
|
123
|
+
c = @counts[key] ||= {}
|
|
124
|
+
c[domain] = counts
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def add_required_columns(op)
|
|
128
|
+
@query_columns |= op.required_columns if op.required_columns
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def harvest_person_ids(op)
|
|
132
|
+
if (person_ids = op.options[:person_ids])
|
|
133
|
+
opts[:person_ids] = person_ids
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def nest(op)
|
|
138
|
+
add_required_columns(op)
|
|
139
|
+
harvest_person_ids(op)
|
|
140
|
+
return yield unless (label = op.is_a?(Operators::Recall) ? op.source : op.label)
|
|
141
|
+
|
|
142
|
+
unless label.is_a?(String)
|
|
143
|
+
op.instance_eval do
|
|
144
|
+
@errors = []
|
|
145
|
+
add_error("invalid label")
|
|
146
|
+
end
|
|
147
|
+
return
|
|
27
148
|
end
|
|
28
|
-
|
|
149
|
+
|
|
150
|
+
recall_dependencies[label] ||= []
|
|
151
|
+
|
|
152
|
+
if nested_recall?(label)
|
|
153
|
+
op.instance_eval do
|
|
154
|
+
@errors = []
|
|
155
|
+
add_error("nested recall")
|
|
156
|
+
end
|
|
157
|
+
return
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
if duplicate_label?(label) && !op.is_a?(Operators::Recall)
|
|
161
|
+
op.instance_eval do
|
|
162
|
+
@errors = []
|
|
163
|
+
add_error("duplicate label")
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
if (last = recall_stack.last)
|
|
168
|
+
recall_dependencies[last] << label
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
begin
|
|
172
|
+
recall_stack.push(label)
|
|
173
|
+
yield
|
|
174
|
+
ensure
|
|
175
|
+
recall_stack.pop if recall_stack.last == label
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def add_operator(operator)
|
|
29
180
|
known_operators[operator.label] = operator
|
|
30
181
|
end
|
|
31
182
|
|
|
183
|
+
def duplicate_label?(label)
|
|
184
|
+
known_operators.keys.map(&:downcase).include?(label.downcase)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def nested_recall?(label)
|
|
188
|
+
recall_stack.map(&:downcase).include?(label.downcase)
|
|
189
|
+
end
|
|
190
|
+
|
|
32
191
|
def from(db, label)
|
|
33
|
-
|
|
192
|
+
if ConceptQL.avoid_ctes?
|
|
193
|
+
ds = db.from(fetch_operator(label).evaluate(db).as(label_cte_name(label)))
|
|
194
|
+
else
|
|
195
|
+
ds = db.from(label_cte_name(label))
|
|
196
|
+
|
|
197
|
+
if ENV["CONCEPTQL_CHECK_COLUMNS"]
|
|
198
|
+
# Work around requests for columns by operators. These
|
|
199
|
+
# would fail because the CTE would not be defined. You
|
|
200
|
+
# don't want to define the CTE normally, but to allow the
|
|
201
|
+
# columns to still work, send the columns request to the
|
|
202
|
+
# underlying operator.
|
|
203
|
+
op = fetch_operator(label)
|
|
204
|
+
ds = ds.with_extend do
|
|
205
|
+
define_method(:columns) do
|
|
206
|
+
(@main_op ||= op.evaluate(db)).columns
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
ds
|
|
34
213
|
end
|
|
35
214
|
|
|
36
|
-
def
|
|
37
|
-
fetch_operator(label).
|
|
215
|
+
def domains(label, db)
|
|
216
|
+
fetch_operator(label).domains(db)
|
|
217
|
+
rescue StandardError
|
|
218
|
+
[ :invalid ]
|
|
38
219
|
end
|
|
39
220
|
|
|
40
|
-
def
|
|
41
|
-
|
|
42
|
-
|
|
221
|
+
def sort_ctes(sorted, unsorted, deps)
|
|
222
|
+
return sorted if unsorted.empty?
|
|
223
|
+
|
|
224
|
+
add, unsorted = unsorted.partition do |label, _|
|
|
225
|
+
deps[label].empty?
|
|
43
226
|
end
|
|
227
|
+
|
|
228
|
+
sorted += add
|
|
229
|
+
|
|
230
|
+
new_deps = {}
|
|
231
|
+
deps.map do |label, dps|
|
|
232
|
+
new_deps[label] = dps - sorted.map(&:first)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
sort_ctes(sorted, unsorted, new_deps)
|
|
44
236
|
end
|
|
45
237
|
|
|
46
|
-
def
|
|
47
|
-
|
|
48
|
-
|
|
238
|
+
def valid?
|
|
239
|
+
recall_dependencies.each_value do |deps|
|
|
240
|
+
return false unless (deps - known_operators.keys).empty?
|
|
49
241
|
end
|
|
242
|
+
true
|
|
50
243
|
end
|
|
51
244
|
|
|
52
|
-
|
|
53
|
-
|
|
245
|
+
class CteExtractor < Sequel::ASTTransformer
|
|
246
|
+
def initialize(scope, ctes)
|
|
247
|
+
super()
|
|
248
|
+
@scope = scope
|
|
249
|
+
@ctes = ctes
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
private
|
|
54
253
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
254
|
+
def v(o)
|
|
255
|
+
if o.is_a?(Sequel::Dataset)
|
|
256
|
+
@scope.recursive_extract_ctes(o, @ctes)
|
|
257
|
+
else
|
|
258
|
+
super
|
|
259
|
+
end
|
|
260
|
+
end
|
|
59
261
|
end
|
|
60
262
|
|
|
61
|
-
def
|
|
62
|
-
|
|
263
|
+
def recursive_extract_cte_expr(t, ctes)
|
|
264
|
+
case t
|
|
265
|
+
when Sequel::Dataset
|
|
266
|
+
recursive_extract_ctes(t, ctes)
|
|
267
|
+
when Sequel::SQL::AliasedExpression
|
|
268
|
+
if t.expression.is_a?(Sequel::Dataset)
|
|
269
|
+
Sequel.as(recursive_extract_ctes(t.expression, ctes), t.alias)
|
|
270
|
+
else
|
|
271
|
+
t
|
|
272
|
+
end
|
|
273
|
+
else
|
|
274
|
+
t
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def recursive_extract_ctes(query, ctes)
|
|
279
|
+
# puts
|
|
280
|
+
# p [:rec, ctes, query.opts]
|
|
281
|
+
|
|
282
|
+
if (with = query.opts[:with])
|
|
283
|
+
keep, remove = with.partition { |w| w[:no_temp_table] }
|
|
284
|
+
ctes.concat(remove.map do |w|
|
|
285
|
+
opts = w.except(:name, :dataset, :no_temp_table)
|
|
286
|
+
# A shared table is created from its whole list, nested CTEs included.
|
|
287
|
+
ds = w[SHARED_TABLE] ? w[:dataset] : recursive_extract_ctes(w[:dataset], ctes)
|
|
288
|
+
[ w[:name], ds, opts ]
|
|
289
|
+
end)
|
|
290
|
+
# p [:rec_with, ctes.map(&:first), with]
|
|
291
|
+
query = query.clone(with: keep.empty? ? nil : keep)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
if (from = query.opts[:from])
|
|
295
|
+
query = query.clone(from: from.map { |t| recursive_extract_cte_expr(t, ctes) })
|
|
296
|
+
# p [:rec_from, ctes.map(&:first), from]
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
if (joins = query.opts[:join])
|
|
300
|
+
query = query.clone(join: joins.map do |jc|
|
|
301
|
+
table_expr = recursive_extract_cte_expr(jc.table_expr, ctes)
|
|
302
|
+
# A CROSS JOIN is a plain JoinClause, with no ON condition.
|
|
303
|
+
if jc.respond_to?(:on)
|
|
304
|
+
jc.class.new(jc.on, jc.join_type, table_expr)
|
|
305
|
+
else
|
|
306
|
+
jc.class.new(jc.join_type, table_expr)
|
|
307
|
+
end
|
|
308
|
+
end)
|
|
309
|
+
# p [:rec_join, ctes.map(&:first), joins]
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
if (compounds = query.opts[:compounds])
|
|
313
|
+
query = query.clone(compounds: compounds.map { |t, ds, a| [ t, recursive_extract_ctes(ds, ctes), a ] })
|
|
314
|
+
# p [:rec_compounds, ctes.map(&:first), compounds]
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
if (where = query.opts[:where])
|
|
318
|
+
query = query.clone(where: CteExtractor.new(self, ctes).transform(where))
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
query
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# Removes duplicate CTE names, then moves each CTE after the CTEs it reads.
|
|
325
|
+
#
|
|
326
|
+
# A CTE reads another when its SQL contains the other's name as the
|
|
327
|
+
# adapter quotes it (ds.db.literal): "name" on Postgres and DuckDB,
|
|
328
|
+
# `name` on Spark, and "scratch"."name" for a temp table in a scratch
|
|
329
|
+
# database. The closing quote stops a name from matching the start of a
|
|
330
|
+
# longer name. Each name also ends with the scope's counter and 32 random
|
|
331
|
+
# hex digits, so a name does not appear in other SQL by accident.
|
|
332
|
+
#
|
|
333
|
+
# The sort is stable: each step takes the first CTE whose dependencies are
|
|
334
|
+
# already placed. recursive_extract_ctes appends the CTEs that a dataset
|
|
335
|
+
# reads before the dataset itself, so an order that is already correct
|
|
336
|
+
# does not change. (sort_ctes sorts by layers, and would move CTEs.)
|
|
337
|
+
def sort_extracted_ctes(ctes)
|
|
338
|
+
unsorted = ctes.uniq(&:first)
|
|
339
|
+
literals = unsorted.to_h { |name, ds| [ name, ds.db.literal(name) ] }
|
|
340
|
+
deps = unsorted.to_h do |name, ds|
|
|
341
|
+
sql = ds.sql
|
|
342
|
+
[ name, literals.filter_map { |other, literal| other if other != name && sql.include?(literal) } ]
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
sorted = []
|
|
346
|
+
until unsorted.empty?
|
|
347
|
+
placed = sorted.map(&:first)
|
|
348
|
+
index = unsorted.index { |name, _| (deps[name] - placed).empty? }
|
|
349
|
+
raise(CteCycleError, unsorted.map { |name, _| literals[name] }) unless index
|
|
350
|
+
|
|
351
|
+
sorted << unsorted.delete_at(index)
|
|
352
|
+
end
|
|
353
|
+
sorted
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def with_ctes(op, db, options = {})
|
|
357
|
+
raise "recall operator use without matching label" unless valid?
|
|
358
|
+
|
|
359
|
+
query = op.evaluate(db)
|
|
360
|
+
rdbms = op.rdbms
|
|
361
|
+
|
|
362
|
+
query = query.from_self
|
|
363
|
+
temp_tables = []
|
|
364
|
+
ctes.each do |label, operator|
|
|
365
|
+
temp_tables << [ label_cte_name(label), recursive_extract_ctes(operator.evaluate(db), temp_tables) ]
|
|
366
|
+
end
|
|
367
|
+
query = recursive_extract_ctes(query, temp_tables)
|
|
368
|
+
shared_tables, temp_tables = temp_tables.partition { |_, _, with_opts| with_opts&.fetch(SHARED_TABLE, false) }
|
|
369
|
+
shared_tables = shared_tables.uniq(&:first)
|
|
370
|
+
temp_tables = sort_extracted_ctes(temp_tables)
|
|
371
|
+
|
|
372
|
+
if force_temp_tables?(options)
|
|
373
|
+
scope = self
|
|
374
|
+
query = query.with_extend do
|
|
375
|
+
# Create temp tables for each CTE
|
|
376
|
+
#
|
|
377
|
+
# Need to override multiple methods if sequel_pg is in use, as in
|
|
378
|
+
# that case not every method calls each. For other adapters or
|
|
379
|
+
# when sequel_pg is not in use, it is probably safe to override just each.
|
|
380
|
+
# There are other methods that may need to be overridden in order to handle
|
|
381
|
+
# all cases when sequel_pg is in use.
|
|
382
|
+
%i[each to_hash_groups to_hash].each do |meth|
|
|
383
|
+
define_method(meth) do |*args, &block|
|
|
384
|
+
if !temp_tables.empty? && !opts[:conceptql_temp_tables_created]
|
|
385
|
+
begin
|
|
386
|
+
temp_tables.each do |table_name, ds, _with_opts|
|
|
387
|
+
db.create_table(table_name, rdbms.create_options(scope, ds).merge(options).merge(as: ds))
|
|
388
|
+
rdbms.post_create(db, table_name)
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
clone(conceptql_temp_tables_created: true).send(meth, *args, &block)
|
|
392
|
+
ensure
|
|
393
|
+
drop_temp_tables
|
|
394
|
+
end
|
|
395
|
+
else
|
|
396
|
+
super(*args, &block)
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
define_method(:columns) do
|
|
402
|
+
if self.db.respond_to?(:record_table)
|
|
403
|
+
temp_tables.each do |table_name, ds, _with_opts|
|
|
404
|
+
self.db.record_table(table_name, self.db.columns_from_sql(ds))
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
super()
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
define_method(:sql_statements) do |*args, &block|
|
|
411
|
+
sql_statements = temp_tables.map do |table_name, ds|
|
|
412
|
+
[ table_name, ds.sql ]
|
|
413
|
+
end.compact.push([ :query, sql(*args, &block) ])
|
|
414
|
+
Hash[scope.shared_table_statements(shared_tables, op) + sql_statements]
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
define_method(:drop_temp_tables) do |opts = {}|
|
|
418
|
+
opts ||= {}
|
|
419
|
+
table_names = temp_tables.reverse_each.map(&:first)
|
|
420
|
+
begin
|
|
421
|
+
db.drop_table?(*table_names, rdbms.drop_options.merge(opts))
|
|
422
|
+
rescue Sequel::DatabaseError
|
|
423
|
+
warn("Unable to drop scratch table: #{literal(*table_names)}")
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
else
|
|
428
|
+
unless ConceptQL.avoid_ctes?
|
|
429
|
+
temp_tables.each do |table_name, ds, with_opts|
|
|
430
|
+
query = if with_opts.nil? || with_opts.empty?
|
|
431
|
+
query.with(table_name, ds)
|
|
432
|
+
else
|
|
433
|
+
query.with(table_name, ds, with_opts)
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
scope = self
|
|
439
|
+
query = query.with_extend do
|
|
440
|
+
define_method(:sql_statements) do |*args, &block|
|
|
441
|
+
Hash[scope.shared_table_statements(shared_tables, op).push([ :query, sql(*args, &block) ])]
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
define_method(:drop_temp_tables) do |opts = {}|
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
shared_tables.empty? ? query : create_shared_tables_on_read(query, shared_tables, op)
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
# The sql_statements entries that create the shared tables that do not
|
|
453
|
+
# exist yet. They come first, because the other entries read them.
|
|
454
|
+
def shared_table_statements(shared_tables, op)
|
|
455
|
+
shared_tables.filter_map { |name, key, _with_opts| op.nodifier.cdb.shared_utilization_statement(name, key) }
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
# Creates the shared tables that do not exist yet before the query reads
|
|
459
|
+
# its first rows, for a caller that reads rows through conceptql. This
|
|
460
|
+
# hook runs before the force_temp_tables hook, so the temp tables can read
|
|
461
|
+
# them. The shared tables are never in drop_temp_tables.
|
|
462
|
+
def create_shared_tables_on_read(query, shared_tables, op)
|
|
463
|
+
scope = self
|
|
464
|
+
cdb = op.nodifier.cdb
|
|
465
|
+
rdbms = op.rdbms
|
|
466
|
+
query.with_extend do
|
|
467
|
+
%i[each to_hash_groups to_hash].each do |meth|
|
|
468
|
+
define_method(meth) do |*args, &block|
|
|
469
|
+
shared_tables.each do |name, key, _with_opts|
|
|
470
|
+
cdb.create_shared_utilization_table(name, key, rdbms, scope)
|
|
471
|
+
end
|
|
472
|
+
super(*args, &block)
|
|
473
|
+
end
|
|
474
|
+
end
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
def label_cte_name(label)
|
|
479
|
+
@label_cte_names[label] ||= cte_name(label)
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
def ctes
|
|
483
|
+
@ctes ||= sort_ctes([], known_operators, recall_dependencies)
|
|
63
484
|
end
|
|
64
485
|
|
|
65
486
|
def fetch_operator(label)
|
|
66
|
-
known_operators[label]
|
|
487
|
+
known_operators[label]
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
def windows
|
|
491
|
+
@windows ||= Window.from(opts.merge(opts[:window_opts] || {}))
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
def force_temp_tables?(options = {})
|
|
495
|
+
# opts.fetch(:force_temp_tables, opts[:force_temp_tables])
|
|
496
|
+
options.fetch(:force_temp_tables, opts[:force_temp_tables])
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
def scratch_database
|
|
500
|
+
opts[:scratch_database]
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
def cte_name(name, options = {})
|
|
504
|
+
cte_parts = [
|
|
505
|
+
opts[:table_prefix],
|
|
506
|
+
name.to_s.gsub(/\W+/, "_"),
|
|
507
|
+
$PROCESS_ID,
|
|
508
|
+
@cte_name_next.call,
|
|
509
|
+
SecureRandom.hex(16)
|
|
510
|
+
].compact
|
|
511
|
+
|
|
512
|
+
name = Sequel.identifier(cte_parts.join("_"))
|
|
513
|
+
|
|
514
|
+
name = name.qualify(scratch_database) if force_temp_tables?(options) && scratch_database
|
|
515
|
+
|
|
516
|
+
name
|
|
67
517
|
end
|
|
68
518
|
end
|
|
69
519
|
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ConceptQL
|
|
4
|
+
module SqlFormatters
|
|
5
|
+
# An external SQL formatter, given as a command array that reads SQL on
|
|
6
|
+
# stdin, e.g. Formatter.new(%w[sql-formatter -l postgresql]).
|
|
7
|
+
class Formatter
|
|
8
|
+
attr_reader :command
|
|
9
|
+
|
|
10
|
+
def initialize(command)
|
|
11
|
+
@command = command
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def available?
|
|
15
|
+
system("which", command.first, out: File::NULL, err: File::NULL)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def format(sql)
|
|
19
|
+
ConceptQL::Utils.timed_capture(*command, stdin_data: sql, timeout: 10)
|
|
20
|
+
rescue Timeout::Error
|
|
21
|
+
sql
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "sql_formatters/formatter"
|
|
4
|
+
|
|
5
|
+
module ConceptQL
|
|
6
|
+
module SqlFormatters
|
|
7
|
+
SQL_FORMATTER = %w[sql-formatter].freeze
|
|
8
|
+
POSTGRES = %w[sql-formatter -l postgresql].freeze
|
|
9
|
+
SQLFORMAT = %w[sqlformat --reindent --use_space_around_operators --identifiers lower --keywords upper -].freeze
|
|
10
|
+
PG_FORMAT = %w[pg_format].freeze
|
|
11
|
+
|
|
12
|
+
# Formats with the rdbms's preferred formatter, else the first of the
|
|
13
|
+
# fallbacks that is installed, else returns the SQL untouched.
|
|
14
|
+
def self.format(sql, rdbms)
|
|
15
|
+
formatter = [ rdbms.preferred_formatter, SQL_FORMATTER, SQLFORMAT, PG_FORMAT ]
|
|
16
|
+
.compact
|
|
17
|
+
.map { |command| Formatter.new(command) }
|
|
18
|
+
.detect(&:available?)
|
|
19
|
+
formatter ? formatter.format(sql) : sql
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|