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.
Files changed (81) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +244 -0
  4. data/docs/drafts/turbo-streams-is-cache-invalidation-you-write-by-hand.md +240 -0
  5. data/docs/how-it-works.md +329 -0
  6. data/docs/plans/sqlglot-active-record-migration.md +211 -0
  7. data/docs/plans/turbo-frame-subscription-composition.md +90 -0
  8. data/docs/spikes/SQLGLOT_VS_AREL_FINDINGS.md +136 -0
  9. data/docs/spikes/query-source-analysis-comparison/FINDINGS.md +169 -0
  10. data/docs/spikes/sqlglot-first-active-record/README.md +26 -0
  11. data/docs/spikes/sqlglot-query-analysis/FINDINGS.md +110 -0
  12. data/docs/spikes/sqlglot-query-analysis/PULSE_1802_FINDINGS.md +95 -0
  13. data/docs/spikes/sqlglot-semantic-bindings/README.md +29 -0
  14. data/lib/generators/upkeep/install/install_generator.rb +192 -0
  15. data/lib/generators/upkeep/install/templates/create_upkeep_subscriptions.rb.erb +49 -0
  16. data/lib/generators/upkeep/install/templates/subscription.js +288 -0
  17. data/lib/generators/upkeep/install/templates/upkeep.rb +65 -0
  18. data/lib/upkeep/active_record_query.rb +248 -0
  19. data/lib/upkeep/capture/request.rb +150 -0
  20. data/lib/upkeep/dag/subscription_shape.rb +244 -0
  21. data/lib/upkeep/dag.rb +454 -0
  22. data/lib/upkeep/delivery/action_cable_adapter.rb +48 -0
  23. data/lib/upkeep/delivery/async_dispatcher.rb +102 -0
  24. data/lib/upkeep/delivery/broadcast_transport.rb +89 -0
  25. data/lib/upkeep/delivery/transport.rb +194 -0
  26. data/lib/upkeep/delivery/turbo_streams.rb +339 -0
  27. data/lib/upkeep/delivery.rb +7 -0
  28. data/lib/upkeep/dependencies.rb +600 -0
  29. data/lib/upkeep/herb/developer_report.rb +135 -0
  30. data/lib/upkeep/herb/manifest_cache.rb +83 -0
  31. data/lib/upkeep/herb/manifest_diff.rb +183 -0
  32. data/lib/upkeep/herb/source_instrumenter.rb +149 -0
  33. data/lib/upkeep/herb/template_manifest.rb +548 -0
  34. data/lib/upkeep/invalidation/collection_append.rb +84 -0
  35. data/lib/upkeep/invalidation/collection_member_replace.rb +78 -0
  36. data/lib/upkeep/invalidation/collection_prepend.rb +84 -0
  37. data/lib/upkeep/invalidation/collection_remove.rb +57 -0
  38. data/lib/upkeep/invalidation/planner.rb +411 -0
  39. data/lib/upkeep/invalidation.rb +7 -0
  40. data/lib/upkeep/rails/action_view_capture.rb +1007 -0
  41. data/lib/upkeep/rails/activation_token.rb +55 -0
  42. data/lib/upkeep/rails/cable/channel.rb +165 -0
  43. data/lib/upkeep/rails/cable/subscriber_identity.rb +361 -0
  44. data/lib/upkeep/rails/cable.rb +4 -0
  45. data/lib/upkeep/rails/client_subscription.rb +65 -0
  46. data/lib/upkeep/rails/cluster_guard.rb +57 -0
  47. data/lib/upkeep/rails/configuration.rb +252 -0
  48. data/lib/upkeep/rails/controller_runtime.rb +187 -0
  49. data/lib/upkeep/rails/install.rb +28 -0
  50. data/lib/upkeep/rails/job_runtime.rb +43 -0
  51. data/lib/upkeep/rails/railtie.rb +44 -0
  52. data/lib/upkeep/rails/replay.rb +244 -0
  53. data/lib/upkeep/rails/testing.rb +259 -0
  54. data/lib/upkeep/rails.rb +466 -0
  55. data/lib/upkeep/replay.rb +462 -0
  56. data/lib/upkeep/runtime.rb +1276 -0
  57. data/lib/upkeep/shared_streams.rb +86 -0
  58. data/lib/upkeep/sql_dependency_analysis.rb +553 -0
  59. data/lib/upkeep/sqlglot/libsqlglot_rust.so +0 -0
  60. data/lib/upkeep/sqlglot/native.rb +121 -0
  61. data/lib/upkeep/sqlglot/native_library.rb +23 -0
  62. data/lib/upkeep/sqlglot.rb +367 -0
  63. data/lib/upkeep/subscriptions/active_record_store.rb +398 -0
  64. data/lib/upkeep/subscriptions/active_record_subscription_persistence.rb +411 -0
  65. data/lib/upkeep/subscriptions/active_registry.rb +80 -0
  66. data/lib/upkeep/subscriptions/base_store.rb +110 -0
  67. data/lib/upkeep/subscriptions/json_snapshot.rb +98 -0
  68. data/lib/upkeep/subscriptions/layered_reverse_index.rb +125 -0
  69. data/lib/upkeep/subscriptions/lookup_instrumentation.rb +32 -0
  70. data/lib/upkeep/subscriptions/persistent_reverse_index.rb +228 -0
  71. data/lib/upkeep/subscriptions/registrar.rb +36 -0
  72. data/lib/upkeep/subscriptions/reverse_index.rb +313 -0
  73. data/lib/upkeep/subscriptions/shape.rb +117 -0
  74. data/lib/upkeep/subscriptions/store.rb +349 -0
  75. data/lib/upkeep/subscriptions.rb +7 -0
  76. data/lib/upkeep/targeting.rb +146 -0
  77. data/lib/upkeep/version.rb +5 -0
  78. data/lib/upkeep-rails.rb +3 -0
  79. data/lib/upkeep.rb +15 -0
  80. data/upkeep-rails.gemspec +66 -0
  81. 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