upkeep-rails 0.2.5-aarch64-linux-gnu
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +244 -0
- data/docs/drafts/turbo-streams-is-cache-invalidation-you-write-by-hand.md +240 -0
- data/docs/how-it-works.md +329 -0
- data/docs/plans/sqlglot-active-record-migration.md +211 -0
- data/docs/plans/turbo-frame-subscription-composition.md +90 -0
- data/docs/spikes/SQLGLOT_VS_AREL_FINDINGS.md +136 -0
- data/docs/spikes/query-source-analysis-comparison/FINDINGS.md +169 -0
- data/docs/spikes/sqlglot-first-active-record/README.md +26 -0
- data/docs/spikes/sqlglot-query-analysis/FINDINGS.md +110 -0
- data/docs/spikes/sqlglot-query-analysis/PULSE_1802_FINDINGS.md +95 -0
- data/docs/spikes/sqlglot-semantic-bindings/README.md +29 -0
- data/lib/generators/upkeep/install/install_generator.rb +192 -0
- data/lib/generators/upkeep/install/templates/create_upkeep_subscriptions.rb.erb +49 -0
- data/lib/generators/upkeep/install/templates/subscription.js +288 -0
- data/lib/generators/upkeep/install/templates/upkeep.rb +65 -0
- data/lib/upkeep/active_record_query.rb +248 -0
- data/lib/upkeep/capture/request.rb +150 -0
- data/lib/upkeep/dag/subscription_shape.rb +244 -0
- data/lib/upkeep/dag.rb +454 -0
- data/lib/upkeep/delivery/action_cable_adapter.rb +48 -0
- data/lib/upkeep/delivery/async_dispatcher.rb +102 -0
- data/lib/upkeep/delivery/broadcast_transport.rb +89 -0
- data/lib/upkeep/delivery/transport.rb +194 -0
- data/lib/upkeep/delivery/turbo_streams.rb +339 -0
- data/lib/upkeep/delivery.rb +7 -0
- data/lib/upkeep/dependencies.rb +600 -0
- data/lib/upkeep/herb/developer_report.rb +135 -0
- data/lib/upkeep/herb/manifest_cache.rb +83 -0
- data/lib/upkeep/herb/manifest_diff.rb +183 -0
- data/lib/upkeep/herb/source_instrumenter.rb +149 -0
- data/lib/upkeep/herb/template_manifest.rb +548 -0
- data/lib/upkeep/invalidation/collection_append.rb +84 -0
- data/lib/upkeep/invalidation/collection_member_replace.rb +78 -0
- data/lib/upkeep/invalidation/collection_prepend.rb +84 -0
- data/lib/upkeep/invalidation/collection_remove.rb +57 -0
- data/lib/upkeep/invalidation/planner.rb +411 -0
- data/lib/upkeep/invalidation.rb +7 -0
- data/lib/upkeep/rails/action_view_capture.rb +1007 -0
- data/lib/upkeep/rails/activation_token.rb +55 -0
- data/lib/upkeep/rails/cable/channel.rb +165 -0
- data/lib/upkeep/rails/cable/subscriber_identity.rb +361 -0
- data/lib/upkeep/rails/cable.rb +4 -0
- data/lib/upkeep/rails/client_subscription.rb +65 -0
- data/lib/upkeep/rails/cluster_guard.rb +57 -0
- data/lib/upkeep/rails/configuration.rb +252 -0
- data/lib/upkeep/rails/controller_runtime.rb +187 -0
- data/lib/upkeep/rails/install.rb +28 -0
- data/lib/upkeep/rails/job_runtime.rb +43 -0
- data/lib/upkeep/rails/railtie.rb +44 -0
- data/lib/upkeep/rails/replay.rb +244 -0
- data/lib/upkeep/rails/testing.rb +259 -0
- data/lib/upkeep/rails.rb +466 -0
- data/lib/upkeep/replay.rb +462 -0
- data/lib/upkeep/runtime.rb +1276 -0
- data/lib/upkeep/shared_streams.rb +86 -0
- data/lib/upkeep/sql_dependency_analysis.rb +553 -0
- data/lib/upkeep/sqlglot/libsqlglot_rust.so +0 -0
- data/lib/upkeep/sqlglot/native.rb +121 -0
- data/lib/upkeep/sqlglot/native_library.rb +23 -0
- data/lib/upkeep/sqlglot.rb +367 -0
- data/lib/upkeep/subscriptions/active_record_store.rb +398 -0
- data/lib/upkeep/subscriptions/active_record_subscription_persistence.rb +411 -0
- data/lib/upkeep/subscriptions/active_registry.rb +80 -0
- data/lib/upkeep/subscriptions/base_store.rb +110 -0
- data/lib/upkeep/subscriptions/json_snapshot.rb +98 -0
- data/lib/upkeep/subscriptions/layered_reverse_index.rb +125 -0
- data/lib/upkeep/subscriptions/lookup_instrumentation.rb +32 -0
- data/lib/upkeep/subscriptions/persistent_reverse_index.rb +228 -0
- data/lib/upkeep/subscriptions/registrar.rb +36 -0
- data/lib/upkeep/subscriptions/reverse_index.rb +313 -0
- data/lib/upkeep/subscriptions/shape.rb +117 -0
- data/lib/upkeep/subscriptions/store.rb +349 -0
- data/lib/upkeep/subscriptions.rb +7 -0
- data/lib/upkeep/targeting.rb +146 -0
- data/lib/upkeep/version.rb +5 -0
- data/lib/upkeep-rails.rb +3 -0
- data/lib/upkeep.rb +15 -0
- data/upkeep-rails.gemspec +66 -0
- metadata +327 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ffi"
|
|
4
|
+
require_relative "native_library"
|
|
5
|
+
|
|
6
|
+
module Upkeep
|
|
7
|
+
module SQLGlot
|
|
8
|
+
module Native
|
|
9
|
+
extend FFI::Library
|
|
10
|
+
|
|
11
|
+
LIB_NAME = "sqlglot_rust"
|
|
12
|
+
SOEXT = FFI::Platform::LIBSUFFIX
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def find_library
|
|
17
|
+
override = ENV["UPKEEP_SQLGLOT_LIB_PATH"]
|
|
18
|
+
return override if override && File.file?(override)
|
|
19
|
+
|
|
20
|
+
gem_root = File.expand_path("../../..", __dir__)
|
|
21
|
+
installed = NativeLibrary.find(__dir__, extensions: [SOEXT])
|
|
22
|
+
return installed if installed
|
|
23
|
+
|
|
24
|
+
development = NativeLibrary.find(
|
|
25
|
+
File.join(
|
|
26
|
+
gem_root,
|
|
27
|
+
"ext",
|
|
28
|
+
"sqlglot_rust",
|
|
29
|
+
"sql-glot-rust",
|
|
30
|
+
"target",
|
|
31
|
+
"release"
|
|
32
|
+
),
|
|
33
|
+
extensions: [SOEXT]
|
|
34
|
+
)
|
|
35
|
+
development || LIB_NAME
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
begin
|
|
39
|
+
ffi_lib find_library
|
|
40
|
+
rescue LoadError => error
|
|
41
|
+
raise LibraryNotFoundError,
|
|
42
|
+
"Could not load lib#{LIB_NAME}: #{error.message}. " \
|
|
43
|
+
"Perform a full gem reinstall so Upkeep's native library is present."
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
attach_function :sqlglot_parse, %i[string string], :pointer
|
|
47
|
+
attach_function :sqlglot_transpile, %i[string string string], :pointer
|
|
48
|
+
attach_function :sqlglot_generate, %i[string string], :pointer
|
|
49
|
+
attach_function :sqlglot_version, [], :string
|
|
50
|
+
attach_function :sqlglot_qualify_columns,
|
|
51
|
+
%i[string string string],
|
|
52
|
+
:pointer
|
|
53
|
+
attach_function :sqlglot_build_scope, [:string], :pointer
|
|
54
|
+
attach_function :sqlglot_lineage,
|
|
55
|
+
%i[string string string string],
|
|
56
|
+
:pointer
|
|
57
|
+
attach_function :sqlglot_free, [:pointer], :void
|
|
58
|
+
|
|
59
|
+
def parse(sql, dialect)
|
|
60
|
+
read_owned(
|
|
61
|
+
sqlglot_parse(sql, dialect),
|
|
62
|
+
ParseError,
|
|
63
|
+
"Failed to parse SQL: #{sql.inspect}"
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def transpile(sql, from_dialect, to_dialect)
|
|
68
|
+
read_owned(
|
|
69
|
+
sqlglot_transpile(sql, from_dialect, to_dialect),
|
|
70
|
+
TranspileError,
|
|
71
|
+
"Failed to transpile SQL: #{sql.inspect}"
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def generate(statement_json, dialect)
|
|
76
|
+
read_owned(
|
|
77
|
+
sqlglot_generate(statement_json, dialect),
|
|
78
|
+
GenerateError,
|
|
79
|
+
"Failed to generate SQL from AST"
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def version
|
|
84
|
+
sqlglot_version
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def qualify_columns(statement_json, schema_json, dialect)
|
|
88
|
+
read_owned(
|
|
89
|
+
sqlglot_qualify_columns(statement_json, schema_json, dialect),
|
|
90
|
+
SemanticError,
|
|
91
|
+
"Failed to qualify SQLGlot columns"
|
|
92
|
+
)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def build_scope(statement_json)
|
|
96
|
+
read_owned(
|
|
97
|
+
sqlglot_build_scope(statement_json),
|
|
98
|
+
SemanticError,
|
|
99
|
+
"Failed to build SQLGlot scope"
|
|
100
|
+
)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def lineage(column, statement_json, schema_json, config_json)
|
|
104
|
+
read_owned(
|
|
105
|
+
sqlglot_lineage(column, statement_json, schema_json, config_json),
|
|
106
|
+
SemanticError,
|
|
107
|
+
"Failed to build SQLGlot lineage for #{column.inspect}"
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def read_owned(pointer, error_class, message)
|
|
112
|
+
raise error_class, message if pointer.null?
|
|
113
|
+
|
|
114
|
+
pointer.read_string.force_encoding(Encoding::UTF_8)
|
|
115
|
+
ensure
|
|
116
|
+
sqlglot_free(pointer) if pointer && !pointer.null?
|
|
117
|
+
end
|
|
118
|
+
private_class_method :read_owned
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Upkeep
|
|
4
|
+
module SQLGlot
|
|
5
|
+
module NativeLibrary
|
|
6
|
+
BASENAME = "sqlglot_rust"
|
|
7
|
+
EXTENSIONS = %w[so dylib dll].freeze
|
|
8
|
+
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def names(extension)
|
|
12
|
+
["lib#{BASENAME}.#{extension}", "#{BASENAME}.#{extension}"]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def find(directory, extensions: EXTENSIONS)
|
|
16
|
+
extensions
|
|
17
|
+
.flat_map { |extension| names(extension) }
|
|
18
|
+
.map { |name| File.join(directory, name) }
|
|
19
|
+
.find { |path| File.file?(path) }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Upkeep
|
|
6
|
+
module SQLGlot
|
|
7
|
+
class Error < StandardError; end
|
|
8
|
+
class LibraryNotFoundError < Error; end
|
|
9
|
+
class ParseError < Error; end
|
|
10
|
+
class TranspileError < Error; end
|
|
11
|
+
class GenerateError < Error; end
|
|
12
|
+
class SemanticError < Error; end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
require_relative "sqlglot/native"
|
|
17
|
+
|
|
18
|
+
module Upkeep
|
|
19
|
+
module SQLGlot
|
|
20
|
+
module Dialect
|
|
21
|
+
NAMES = %w[
|
|
22
|
+
ansi athena bigquery clickhouse databricks doris dremio drill druid
|
|
23
|
+
duckdb exasol fabric hive materialize mysql oracle postgres presto prql
|
|
24
|
+
redshift risingwave singlestore snowflake spark sqlite starrocks tableau
|
|
25
|
+
teradata trino tsql
|
|
26
|
+
].freeze
|
|
27
|
+
ALIASES = {
|
|
28
|
+
"mariadb" => "mysql",
|
|
29
|
+
"mssql" => "tsql",
|
|
30
|
+
"postgresql" => "postgres",
|
|
31
|
+
"sqlserver" => "tsql"
|
|
32
|
+
}.freeze
|
|
33
|
+
|
|
34
|
+
module_function
|
|
35
|
+
|
|
36
|
+
def resolve(name)
|
|
37
|
+
return nil if name.nil?
|
|
38
|
+
|
|
39
|
+
key = name.to_s.downcase.strip
|
|
40
|
+
return key if NAMES.include?(key)
|
|
41
|
+
return ALIASES.fetch(key) if ALIASES.key?(key)
|
|
42
|
+
|
|
43
|
+
raise ArgumentError, "Unknown SQL dialect: #{name.inspect}"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
ColumnRef = Data.define(:table, :name) do
|
|
48
|
+
def self.from_native(value)
|
|
49
|
+
new(table: value["table"], name: value.fetch("name"))
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
TableRef = Data.define(
|
|
54
|
+
:catalog,
|
|
55
|
+
:schema,
|
|
56
|
+
:name,
|
|
57
|
+
:alias,
|
|
58
|
+
:name_quote_style,
|
|
59
|
+
:alias_quote_style
|
|
60
|
+
) do
|
|
61
|
+
def self.from_native(value)
|
|
62
|
+
new(
|
|
63
|
+
catalog: value["catalog"],
|
|
64
|
+
schema: value["schema"],
|
|
65
|
+
name: value.fetch("name"),
|
|
66
|
+
alias: value["alias"],
|
|
67
|
+
name_quote_style: value["name_quote_style"],
|
|
68
|
+
alias_quote_style: value["alias_quote_style"]
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def qualified_name
|
|
73
|
+
[catalog, schema, name].compact.reject(&:empty?).join(".")
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
Source = Data.define(:kind, :table, :scope) do
|
|
78
|
+
def self.from_native(value)
|
|
79
|
+
case value.fetch("kind")
|
|
80
|
+
when "table"
|
|
81
|
+
new(
|
|
82
|
+
kind: :table,
|
|
83
|
+
table: TableRef.from_native(value.fetch("table")),
|
|
84
|
+
scope: nil
|
|
85
|
+
)
|
|
86
|
+
when "scope"
|
|
87
|
+
new(
|
|
88
|
+
kind: :scope,
|
|
89
|
+
table: nil,
|
|
90
|
+
scope: Scope.from_native(value.fetch("scope"))
|
|
91
|
+
)
|
|
92
|
+
else
|
|
93
|
+
raise KeyError, "Unknown SQLGlot source kind: #{value["kind"].inspect}"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
Scope = Data.define(
|
|
99
|
+
:scope_type,
|
|
100
|
+
:sources,
|
|
101
|
+
:columns,
|
|
102
|
+
:external_columns,
|
|
103
|
+
:derived_table_scopes,
|
|
104
|
+
:subquery_scopes,
|
|
105
|
+
:union_scopes,
|
|
106
|
+
:cte_scopes,
|
|
107
|
+
:selected_sources,
|
|
108
|
+
:is_correlated
|
|
109
|
+
) do
|
|
110
|
+
def self.from_native(value)
|
|
111
|
+
new(
|
|
112
|
+
scope_type: value.fetch("scope_type").to_sym,
|
|
113
|
+
sources: sources_from_native(value.fetch("sources")),
|
|
114
|
+
columns: columns_from_native(value.fetch("columns")),
|
|
115
|
+
external_columns: columns_from_native(value.fetch("external_columns")),
|
|
116
|
+
derived_table_scopes: scopes_from_native(
|
|
117
|
+
value.fetch("derived_table_scopes")
|
|
118
|
+
),
|
|
119
|
+
subquery_scopes: scopes_from_native(value.fetch("subquery_scopes")),
|
|
120
|
+
union_scopes: scopes_from_native(value.fetch("union_scopes")),
|
|
121
|
+
cte_scopes: scopes_from_native(value.fetch("cte_scopes")),
|
|
122
|
+
selected_sources: sources_from_native(value.fetch("selected_sources")),
|
|
123
|
+
is_correlated: value.fetch("is_correlated")
|
|
124
|
+
)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def child_scopes
|
|
128
|
+
derived_table_scopes + subquery_scopes + union_scopes + cte_scopes
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def walk(&block)
|
|
132
|
+
return enum_for(:walk) unless block
|
|
133
|
+
|
|
134
|
+
yield self
|
|
135
|
+
child_scopes.each { |child| child.walk(&block) }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
class << self
|
|
139
|
+
private
|
|
140
|
+
|
|
141
|
+
def columns_from_native(value)
|
|
142
|
+
value.map { |column| ColumnRef.from_native(column) }.freeze
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def sources_from_native(value)
|
|
146
|
+
value.to_h.transform_values { |source| Source.from_native(source) }
|
|
147
|
+
.freeze
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def scopes_from_native(value)
|
|
151
|
+
value.map { |scope| from_native(scope) }.freeze
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
LineageConfig = Data.define(:dialect, :trim_qualifiers, :sources) do
|
|
157
|
+
def initialize(dialect: nil, trim_qualifiers: true, sources: {})
|
|
158
|
+
super(
|
|
159
|
+
dialect: Dialect.resolve(dialect) || "ansi",
|
|
160
|
+
trim_qualifiers: !!trim_qualifiers,
|
|
161
|
+
sources: sources.to_h.transform_keys(&:to_s).transform_values(&:to_s)
|
|
162
|
+
.freeze
|
|
163
|
+
)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def with_sources(value)
|
|
167
|
+
with(sources: value)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def with_trim_qualifiers(value)
|
|
171
|
+
with(trim_qualifiers: value)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def to_h
|
|
175
|
+
{
|
|
176
|
+
dialect: dialect,
|
|
177
|
+
trim_qualifiers: trim_qualifiers,
|
|
178
|
+
sources: sources
|
|
179
|
+
}
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
LineageNode = Data.define(
|
|
184
|
+
:name,
|
|
185
|
+
:expression,
|
|
186
|
+
:source_name,
|
|
187
|
+
:source,
|
|
188
|
+
:downstream,
|
|
189
|
+
:alias,
|
|
190
|
+
:depth
|
|
191
|
+
) do
|
|
192
|
+
def self.from_native(value)
|
|
193
|
+
new(
|
|
194
|
+
name: value.fetch("name"),
|
|
195
|
+
expression: value["expression"],
|
|
196
|
+
source_name: value["source_name"],
|
|
197
|
+
source: value["source"],
|
|
198
|
+
downstream: value.fetch("downstream").map { |child| from_native(child) }
|
|
199
|
+
.freeze,
|
|
200
|
+
alias: value["alias"],
|
|
201
|
+
depth: value.fetch("depth")
|
|
202
|
+
)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def walk(&block)
|
|
206
|
+
return enum_for(:walk) unless block
|
|
207
|
+
|
|
208
|
+
yield self
|
|
209
|
+
downstream.each { |child| child.walk(&block) }
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def source_tables
|
|
213
|
+
walk.filter_map(&:source_name).uniq.sort
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
LineageGraph = Data.define(:node, :sql, :dialect) do
|
|
218
|
+
def self.from_native(value)
|
|
219
|
+
new(
|
|
220
|
+
node: LineageNode.from_native(value.fetch("node")),
|
|
221
|
+
sql: value["sql"],
|
|
222
|
+
dialect: value.fetch("dialect").downcase.to_sym
|
|
223
|
+
)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def source_tables
|
|
227
|
+
node.source_tables
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def walk(&block)
|
|
231
|
+
node.walk(&block)
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
class MappingSchema
|
|
236
|
+
attr_reader :dialect
|
|
237
|
+
|
|
238
|
+
def initialize(mapping = {}, dialect: nil)
|
|
239
|
+
@dialect = Dialect.resolve(dialect) || "ansi"
|
|
240
|
+
@mapping = deep_freeze(stringify_mapping(mapping.to_h))
|
|
241
|
+
@tables = flatten_tables(@mapping).freeze
|
|
242
|
+
freeze
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def to_native
|
|
246
|
+
{tables: native_tables { |_name, data_type| data_type }}
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def to_dependency_native
|
|
250
|
+
{tables: native_tables { "UNKNOWN" }}
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
private
|
|
254
|
+
|
|
255
|
+
def native_tables
|
|
256
|
+
@tables.map do |path, columns|
|
|
257
|
+
{
|
|
258
|
+
path: path,
|
|
259
|
+
columns: columns.map do |name, data_type|
|
|
260
|
+
{name: name, data_type: yield(name, data_type)}
|
|
261
|
+
end
|
|
262
|
+
}
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def flatten_tables(node, prefix = [])
|
|
267
|
+
node.each_with_object([]) do |(name, value), tables|
|
|
268
|
+
path = prefix + Array(name)
|
|
269
|
+
unless (1..3).cover?(path.length)
|
|
270
|
+
raise ArgumentError,
|
|
271
|
+
"Schema table paths must contain one to three identifiers: " \
|
|
272
|
+
"#{path.inspect}"
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
if column_mapping?(value)
|
|
276
|
+
columns = value.map { |column, data_type| [column, data_type.to_s] }
|
|
277
|
+
tables << [path.freeze, columns.freeze]
|
|
278
|
+
else
|
|
279
|
+
tables.concat(flatten_tables(value, path))
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def column_mapping?(value)
|
|
285
|
+
return true if value.empty?
|
|
286
|
+
|
|
287
|
+
nested = value.values.count { |item| item.is_a?(Hash) }
|
|
288
|
+
return false if nested == value.length
|
|
289
|
+
return true if nested.zero?
|
|
290
|
+
|
|
291
|
+
raise ArgumentError,
|
|
292
|
+
"Schema mappings cannot mix namespaces and columns at one level"
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def stringify_mapping(value)
|
|
296
|
+
value.each_with_object({}) do |(key, item), result|
|
|
297
|
+
string_key = if key.is_a?(Array)
|
|
298
|
+
key.map { |part| part.to_s.freeze }.freeze
|
|
299
|
+
else
|
|
300
|
+
key.to_s
|
|
301
|
+
end
|
|
302
|
+
result[string_key] = item.is_a?(Hash) ? stringify_mapping(item) : item.to_s
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def deep_freeze(value)
|
|
307
|
+
value.each do |key, item|
|
|
308
|
+
key.freeze
|
|
309
|
+
item.is_a?(Hash) ? deep_freeze(item) : item.freeze
|
|
310
|
+
end
|
|
311
|
+
value.freeze
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
module_function
|
|
316
|
+
|
|
317
|
+
def parse(sql, dialect: nil)
|
|
318
|
+
JSON.parse(Native.parse(sql.to_s, Dialect.resolve(dialect)))
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def transpile(sql, from: nil, to: nil)
|
|
322
|
+
Native.transpile(
|
|
323
|
+
sql.to_s,
|
|
324
|
+
Dialect.resolve(from),
|
|
325
|
+
Dialect.resolve(to)
|
|
326
|
+
)
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def generate(statement, dialect: nil)
|
|
330
|
+
Native.generate(JSON.generate(statement), Dialect.resolve(dialect))
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def version
|
|
334
|
+
Native.version
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def qualify_columns(statement, schema)
|
|
338
|
+
JSON.parse(
|
|
339
|
+
Native.qualify_columns(
|
|
340
|
+
JSON.generate(statement),
|
|
341
|
+
JSON.generate(schema.to_dependency_native),
|
|
342
|
+
schema.dialect
|
|
343
|
+
)
|
|
344
|
+
)
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def build_scope(statement)
|
|
348
|
+
Scope.from_native(
|
|
349
|
+
JSON.parse(Native.build_scope(JSON.generate(statement)))
|
|
350
|
+
)
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def lineage(column, statement, schema, config = nil)
|
|
354
|
+
config ||= LineageConfig.new(dialect: schema.dialect)
|
|
355
|
+
LineageGraph.from_native(
|
|
356
|
+
JSON.parse(
|
|
357
|
+
Native.lineage(
|
|
358
|
+
column.to_s,
|
|
359
|
+
JSON.generate(statement),
|
|
360
|
+
JSON.generate(schema.to_dependency_native),
|
|
361
|
+
JSON.generate(config.to_h)
|
|
362
|
+
)
|
|
363
|
+
)
|
|
364
|
+
)
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
end
|