pg_objects 1.4.8 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +1 -1
- data/.gitignore +3 -0
- data/.rubocop.yml +13 -2
- data/CHANGELOG.md +78 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +71 -54
- data/README.md +174 -3
- data/Rakefile +2 -0
- data/bin/benchmark +42 -7
- data/bin/console +1 -0
- data/lib/generators/pg_objects/install/install_generator.rb +2 -0
- data/lib/pg_objects/config.rb +53 -2
- data/lib/pg_objects/db_object.rb +6 -1
- data/lib/pg_objects/db_object_factory.rb +11 -3
- data/lib/pg_objects/logger.rb +34 -3
- data/lib/pg_objects/manager.rb +66 -15
- data/lib/pg_objects/parsed_object/aggregate.rb +9 -1
- data/lib/pg_objects/parsed_object/base.rb +38 -0
- data/lib/pg_objects/parsed_object/base_type.rb +17 -0
- data/lib/pg_objects/parsed_object/conversion.rb +9 -1
- data/lib/pg_objects/parsed_object/domain.rb +16 -0
- data/lib/pg_objects/parsed_object/enum_type.rb +16 -0
- data/lib/pg_objects/parsed_object/event_trigger.rb +3 -1
- data/lib/pg_objects/parsed_object/extension.rb +10 -0
- data/lib/pg_objects/parsed_object/function.rb +9 -1
- data/lib/pg_objects/parsed_object/index.rb +17 -0
- data/lib/pg_objects/parsed_object/materialized_view.rb +9 -1
- data/lib/pg_objects/parsed_object/operator.rb +9 -1
- data/lib/pg_objects/parsed_object/operator_class.rb +9 -1
- data/lib/pg_objects/parsed_object/policy.rb +10 -0
- data/lib/pg_objects/parsed_object/range_type.rb +16 -0
- data/lib/pg_objects/parsed_object/rule.rb +10 -0
- data/lib/pg_objects/parsed_object/sequence.rb +16 -0
- data/lib/pg_objects/parsed_object/table.rb +9 -1
- data/lib/pg_objects/parsed_object/text_search_parser.rb +9 -1
- data/lib/pg_objects/parsed_object/text_search_template.rb +9 -1
- data/lib/pg_objects/parsed_object/trigger.rb +3 -1
- data/lib/pg_objects/parsed_object/type.rb +9 -1
- data/lib/pg_objects/parsed_object/view.rb +9 -1
- data/lib/pg_objects/parsed_object.rb +11 -0
- data/lib/pg_objects/parsed_object_factory.rb +52 -90
- data/lib/pg_objects/parser.rb +28 -12
- data/lib/pg_objects/railtie.rb +2 -0
- data/lib/pg_objects/version.rb +3 -1
- data/lib/pg_objects/yaml_configurable.rb +25 -7
- data/lib/pg_objects.rb +54 -4
- data/lib/tasks/pg_objects_tasks.rake +33 -8
- data/pg_objects.gemspec +5 -3
- metadata +16 -20
- data/.github/copilot-instructions.md +0 -131
data/bin/benchmark
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
2
3
|
|
|
3
4
|
require 'bundler/setup'
|
|
5
|
+
require 'active_support/core_ext/string/inflections' # String#classify / #constantize used by the parser
|
|
4
6
|
require 'pg_objects'
|
|
5
7
|
require 'benchmark'
|
|
6
8
|
require 'fileutils'
|
|
@@ -242,6 +244,7 @@ class PgObjectsBenchmark
|
|
|
242
244
|
parser = PgObjects::Parser.new
|
|
243
245
|
successful_parses = 0
|
|
244
246
|
parse_errors = 0
|
|
247
|
+
sample_error = nil
|
|
245
248
|
|
|
246
249
|
result = Benchmark.measure do
|
|
247
250
|
files.each do |file|
|
|
@@ -249,8 +252,9 @@ class PgObjectsBenchmark
|
|
|
249
252
|
begin
|
|
250
253
|
parser.load(content).fetch_object_name
|
|
251
254
|
successful_parses += 1
|
|
252
|
-
rescue StandardError
|
|
255
|
+
rescue StandardError => e
|
|
253
256
|
parse_errors += 1
|
|
257
|
+
sample_error ||= "#{e.class}: #{e.message}"
|
|
254
258
|
end
|
|
255
259
|
end
|
|
256
260
|
end
|
|
@@ -260,12 +264,14 @@ class PgObjectsBenchmark
|
|
|
260
264
|
files_count: files.size,
|
|
261
265
|
successful_parses: successful_parses,
|
|
262
266
|
parse_errors: parse_errors,
|
|
267
|
+
sample_error: sample_error,
|
|
263
268
|
throughput: files.size / result.real
|
|
264
269
|
}
|
|
265
270
|
|
|
266
271
|
puts "Parsed #{files.size} files in #{result.real.round(4)}s"
|
|
267
272
|
puts "Successful parses: #{successful_parses}"
|
|
268
273
|
puts "Parse errors: #{parse_errors}"
|
|
274
|
+
puts " Sample error: #{sample_error}" if sample_error
|
|
269
275
|
puts "Throughput: #{(files.size / result.real).round(2)} files/second"
|
|
270
276
|
puts "Average time per file: #{(result.real / files.size * 1000).round(2)}ms"
|
|
271
277
|
puts
|
|
@@ -308,6 +314,8 @@ class PgObjectsBenchmark
|
|
|
308
314
|
# Test the parsing workflow without database operations
|
|
309
315
|
files = Dir[File.join(@temp_dir, '**', '*.sql')]
|
|
310
316
|
db_objects = []
|
|
317
|
+
workflow_errors = 0
|
|
318
|
+
sample_error = nil
|
|
311
319
|
|
|
312
320
|
result = Benchmark.measure do
|
|
313
321
|
files.each do |file_path|
|
|
@@ -327,18 +335,23 @@ class PgObjectsBenchmark
|
|
|
327
335
|
dependencies: dependencies,
|
|
328
336
|
content: content
|
|
329
337
|
}
|
|
330
|
-
rescue StandardError
|
|
331
|
-
|
|
338
|
+
rescue StandardError => e
|
|
339
|
+
workflow_errors += 1
|
|
340
|
+
sample_error ||= "#{e.class}: #{e.message}"
|
|
332
341
|
end
|
|
333
342
|
end
|
|
334
343
|
|
|
335
344
|
@results[:full_workflow] = {
|
|
336
345
|
time: result.real,
|
|
337
346
|
objects_count: db_objects.size,
|
|
347
|
+
errors: workflow_errors,
|
|
348
|
+
sample_error: sample_error,
|
|
338
349
|
throughput: db_objects.size / result.real
|
|
339
350
|
}
|
|
340
351
|
|
|
341
352
|
puts "Processed #{db_objects.size} objects in #{result.real.round(4)}s"
|
|
353
|
+
puts "Errors: #{workflow_errors}"
|
|
354
|
+
puts " Sample error: #{sample_error}" if sample_error
|
|
342
355
|
puts "Throughput: #{(db_objects.size / result.real).round(2)} objects/second"
|
|
343
356
|
puts "Average time per object: #{(result.real / db_objects.size * 1000).round(2)}ms"
|
|
344
357
|
puts
|
|
@@ -354,6 +367,8 @@ class PgObjectsBenchmark
|
|
|
354
367
|
files = Dir[File.join(@temp_dir, '**', '*.sql')]
|
|
355
368
|
parser = PgObjects::Parser.new
|
|
356
369
|
parsed_objects = []
|
|
370
|
+
memory_errors = 0
|
|
371
|
+
sample_error = nil
|
|
357
372
|
|
|
358
373
|
files.each do |file|
|
|
359
374
|
content = File.read(file)
|
|
@@ -361,11 +376,15 @@ class PgObjectsBenchmark
|
|
|
361
376
|
object_name = parser.load(content).fetch_object_name
|
|
362
377
|
dependencies = parser.fetch_directives[:depends_on]
|
|
363
378
|
parsed_objects << { file: file, name: object_name, deps: dependencies }
|
|
364
|
-
rescue StandardError
|
|
365
|
-
|
|
379
|
+
rescue StandardError => e
|
|
380
|
+
memory_errors += 1
|
|
381
|
+
sample_error ||= "#{e.class}: #{e.message}"
|
|
366
382
|
end
|
|
367
383
|
end
|
|
368
384
|
|
|
385
|
+
puts "Parse errors during memory analysis: #{memory_errors}" if memory_errors.positive?
|
|
386
|
+
puts " Sample error: #{sample_error}" if sample_error
|
|
387
|
+
|
|
369
388
|
end_memory = memory_usage
|
|
370
389
|
memory_diff = end_memory - start_memory
|
|
371
390
|
|
|
@@ -386,9 +405,25 @@ class PgObjectsBenchmark
|
|
|
386
405
|
end
|
|
387
406
|
|
|
388
407
|
def memory_usage
|
|
389
|
-
|
|
408
|
+
rss_from_proc_status || rss_from_ps || 0
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
# Linux: parse VmRSS from /proc/self/status (portable, no shell, locale-safe)
|
|
412
|
+
def rss_from_proc_status
|
|
413
|
+
return nil unless File.readable?('/proc/self/status')
|
|
414
|
+
|
|
415
|
+
kb = File.read('/proc/self/status')[/^VmRSS:\s+(\d+)\s+kB/, 1]
|
|
416
|
+
kb && (kb.to_i * 1024) # Convert KB to bytes
|
|
417
|
+
rescue StandardError
|
|
418
|
+
nil
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
# Portable fallback (e.g. macOS): RSS in KB via ps
|
|
422
|
+
def rss_from_ps
|
|
423
|
+
kb = `ps -o rss= -p #{Process.pid}`.strip
|
|
424
|
+
kb.empty? ? nil : kb.to_i * 1024
|
|
390
425
|
rescue StandardError
|
|
391
|
-
|
|
426
|
+
nil
|
|
392
427
|
end
|
|
393
428
|
|
|
394
429
|
def format_memory(bytes)
|
data/bin/console
CHANGED
data/lib/pg_objects/config.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require_relative 'yaml_configurable'
|
|
2
4
|
|
|
3
5
|
module PgObjects
|
|
@@ -10,7 +12,7 @@ module PgObjects
|
|
|
10
12
|
# # or full
|
|
11
13
|
# config.after_path = '/var/tmp/alternate/after'
|
|
12
14
|
# config.extensions = ['sql', 'txt']
|
|
13
|
-
# # suppress output to console
|
|
15
|
+
# # suppress non-error output to console
|
|
14
16
|
# config.silent = true
|
|
15
17
|
# end
|
|
16
18
|
class << self
|
|
@@ -32,6 +34,55 @@ module PgObjects
|
|
|
32
34
|
setting :extensions, default: ['sql']
|
|
33
35
|
setting :silent, default: false
|
|
34
36
|
|
|
35
|
-
|
|
37
|
+
# Wraps each create_objects run in a database transaction so a failure
|
|
38
|
+
# mid-run rolls back every object created in that run. Set to false to
|
|
39
|
+
# execute statements without a wrapping transaction — required when object
|
|
40
|
+
# files contain statements PostgreSQL rejects inside a transaction block
|
|
41
|
+
# (e.g. CREATE INDEX CONCURRENTLY, VACUUM).
|
|
42
|
+
setting :transactional, default: true
|
|
43
|
+
|
|
44
|
+
# Master switch for the Rake hooks. When false, no hooks are installed and
|
|
45
|
+
# objects are created only by invoking db:create_objects:before / :after
|
|
46
|
+
# manually. Default true for backward compatibility.
|
|
47
|
+
setting :auto_hook_migrations, default: true
|
|
48
|
+
|
|
49
|
+
# Rake tasks that trigger object creation, mapped to the stages they run.
|
|
50
|
+
# +:before+ creates objects from the "before" folder ahead of the task,
|
|
51
|
+
# +:after+ creates objects from the "after" folder once the task finishes.
|
|
52
|
+
# Override (or empty) this to opt out. db:rollback is intentionally absent
|
|
53
|
+
# (a rollback should not recreate objects).
|
|
54
|
+
setting :hook_tasks, default: {
|
|
55
|
+
'db:migrate' => %i[before after],
|
|
56
|
+
'db:schema:load' => %i[after],
|
|
57
|
+
'db:migrate:redo' => %i[before after]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
DEFAULT_YAML_PATH = 'config/pg_objects.yml'
|
|
61
|
+
|
|
62
|
+
class << self
|
|
63
|
+
# YAML loading is deferred to the first config access so the path is
|
|
64
|
+
# resolved against Rails.root (when available) instead of the
|
|
65
|
+
# require-time working directory — preloaders like Spring may require
|
|
66
|
+
# the gem before the process chdirs into the app root.
|
|
67
|
+
def config
|
|
68
|
+
ensure_yaml_loaded
|
|
69
|
+
super
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def ensure_yaml_loaded
|
|
75
|
+
return if @yaml_loaded
|
|
76
|
+
|
|
77
|
+
@yaml_loaded = true
|
|
78
|
+
load_from_yaml(yaml_config_path)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def yaml_config_path
|
|
82
|
+
return DEFAULT_YAML_PATH unless defined?(Rails) && Rails.respond_to?(:root) && Rails.root
|
|
83
|
+
|
|
84
|
+
Rails.root.join(DEFAULT_YAML_PATH).to_s
|
|
85
|
+
end
|
|
86
|
+
end
|
|
36
87
|
end
|
|
37
88
|
end
|
data/lib/pg_objects/db_object.rb
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
##
|
|
2
4
|
# Represents DB object as it is described in file
|
|
3
5
|
#
|
|
4
6
|
# [name] name of file without extension
|
|
5
7
|
# [full_name] full pathname of file
|
|
6
8
|
# [object_name] name of function, trigger etc. if it was successfully parsed, otherwise - nil
|
|
9
|
+
# [qualified_object_name] schema-qualified object name (+schema.name+) when a schema is
|
|
10
|
+
# present, otherwise same as object_name; nil if parsing failed
|
|
7
11
|
class PgObjects::DbObject
|
|
8
12
|
include Memery
|
|
9
13
|
|
|
10
14
|
include Import['parser']
|
|
11
15
|
|
|
12
16
|
attr_accessor :status
|
|
13
|
-
attr_reader :full_name, :object_name
|
|
17
|
+
attr_reader :full_name, :object_name, :qualified_object_name
|
|
14
18
|
|
|
15
19
|
def initialize(path, status = :new, parser:)
|
|
16
20
|
@full_name = path
|
|
@@ -21,6 +25,7 @@ class PgObjects::DbObject
|
|
|
21
25
|
def create
|
|
22
26
|
parser.load(sql_query)
|
|
23
27
|
@object_name = parser.fetch_object_name
|
|
28
|
+
@qualified_object_name = parser.fetch_qualified_object_name
|
|
24
29
|
@status = :pending
|
|
25
30
|
|
|
26
31
|
self
|
|
@@ -3,11 +3,19 @@
|
|
|
3
3
|
##
|
|
4
4
|
# Factory for DbObject
|
|
5
5
|
#
|
|
6
|
+
# Each DbObject gets its own Parser instance so the parser's mutable `@source`
|
|
7
|
+
# is never shared between objects. The parser class is
|
|
8
|
+
# injectable so callers/tests can substitute an alternate parser.
|
|
6
9
|
class PgObjects::DbObjectFactory
|
|
7
|
-
|
|
10
|
+
def initialize(parser_class: PgObjects::Parser)
|
|
11
|
+
@parser_class = parser_class
|
|
12
|
+
end
|
|
8
13
|
|
|
9
14
|
def create_instance(path, status: :new)
|
|
10
|
-
|
|
11
|
-
db_object.create
|
|
15
|
+
PgObjects::DbObject.new(path, status, parser: parser_class.new).create
|
|
12
16
|
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
attr_reader :parser_class
|
|
13
21
|
end
|
data/lib/pg_objects/logger.rb
CHANGED
|
@@ -1,10 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
##
|
|
2
|
-
# Console output
|
|
3
|
-
#
|
|
4
|
+
# Console output with severity levels. Writes to the injected +stream+
|
|
5
|
+
# (default +$stdout+). With +config.silent+ enabled, +info+/+warn+ messages
|
|
6
|
+
# are suppressed; +error+ messages are always written.
|
|
4
7
|
class PgObjects::Logger
|
|
5
8
|
include Import['config']
|
|
6
9
|
|
|
10
|
+
def initialize(stream: $stdout, **deps)
|
|
11
|
+
super(**deps)
|
|
12
|
+
@stream = stream
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Backward-compatible entry point; behaves like +info+.
|
|
7
16
|
def write(str)
|
|
8
|
-
|
|
17
|
+
log(str)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def info(str)
|
|
21
|
+
log(str)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def warn(str)
|
|
25
|
+
log("[WARN] #{str}")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def error(str)
|
|
29
|
+
log("[ERROR] #{str}", force: true)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
attr_reader :stream
|
|
35
|
+
|
|
36
|
+
def log(str, force: false)
|
|
37
|
+
return if config.silent && !force
|
|
38
|
+
|
|
39
|
+
stream.puts "== #{str} ".ljust(80, '=')
|
|
9
40
|
end
|
|
10
41
|
end
|
data/lib/pg_objects/manager.rb
CHANGED
|
@@ -1,23 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
##
|
|
2
4
|
# Manages process to create objects
|
|
3
5
|
#
|
|
4
|
-
# Usage:
|
|
6
|
+
# Usage (dependencies are auto-injected, keyword overrides optional):
|
|
5
7
|
#
|
|
6
|
-
# Manager.new
|
|
8
|
+
# Manager.new.load_files(:before).create_objects
|
|
7
9
|
#
|
|
8
10
|
# or
|
|
9
11
|
#
|
|
10
|
-
# Manager.new(config, logger).load_files(:after).create_objects
|
|
12
|
+
# Manager.new(config: custom_config, logger: custom_logger).load_files(:after).create_objects
|
|
13
|
+
#
|
|
14
|
+
# Pass +connection:+ to run against a specific database connection instead of
|
|
15
|
+
# the global one (Rails 6+ multi-DB):
|
|
16
|
+
#
|
|
17
|
+
# Manager.new(connection: AnimalsRecord.connection).load_files(:before).create_objects
|
|
11
18
|
class PgObjects::Manager
|
|
12
19
|
include Import['db_object_factory', 'config', 'logger']
|
|
13
20
|
|
|
21
|
+
def initialize(connection: nil, **deps)
|
|
22
|
+
super(**deps)
|
|
23
|
+
@connection = connection
|
|
24
|
+
end
|
|
25
|
+
|
|
14
26
|
##
|
|
15
27
|
# event: +:before+ or +:after+
|
|
16
28
|
#
|
|
17
29
|
# used to reference configuration settings +before_path+ and +after_path+
|
|
30
|
+
#
|
|
31
|
+
# Resets the object list before loading, so each call reflects only the
|
|
32
|
+
# files for the given event.
|
|
18
33
|
def load_files(event)
|
|
19
34
|
validate_workability
|
|
20
35
|
|
|
36
|
+
objects.clear
|
|
21
37
|
dir = config.send "#{event}_path"
|
|
22
38
|
Dir[File.join(dir, '**', "*.{#{config.extensions.join(',')}}")].each do |path|
|
|
23
39
|
objects << db_object_factory.create_instance(path)
|
|
@@ -27,7 +43,8 @@ class PgObjects::Manager
|
|
|
27
43
|
end
|
|
28
44
|
|
|
29
45
|
def create_objects
|
|
30
|
-
|
|
46
|
+
build_objects_index
|
|
47
|
+
within_transaction { objects.each { |obj| create_object(obj) } }
|
|
31
48
|
end
|
|
32
49
|
|
|
33
50
|
def objects
|
|
@@ -36,34 +53,68 @@ class PgObjects::Manager
|
|
|
36
53
|
|
|
37
54
|
private
|
|
38
55
|
|
|
56
|
+
def connection
|
|
57
|
+
@connection || ActiveRecord::Base.connection
|
|
58
|
+
end
|
|
59
|
+
|
|
39
60
|
def validate_workability
|
|
40
|
-
raise PgObjects::UnsupportedAdapterError if
|
|
61
|
+
raise PgObjects::UnsupportedAdapterError if connection.adapter_name != 'PostgreSQL'
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def within_transaction(&)
|
|
65
|
+
return yield unless config.transactional
|
|
66
|
+
|
|
67
|
+
connection.transaction(&)
|
|
41
68
|
end
|
|
42
69
|
|
|
43
|
-
def create_object(obj)
|
|
70
|
+
def create_object(obj, stack = [])
|
|
44
71
|
return if obj.status == :done
|
|
45
|
-
raise PgObjects::CyclicDependencyError, obj
|
|
72
|
+
raise PgObjects::CyclicDependencyError, cycle_path(obj, stack) if obj.status == :processing
|
|
46
73
|
|
|
47
74
|
obj.status = :processing
|
|
48
75
|
|
|
49
|
-
create_dependencies(obj.
|
|
76
|
+
create_dependencies(obj, stack + [obj.name])
|
|
50
77
|
|
|
51
78
|
logger.write("creating #{obj.name}")
|
|
52
|
-
|
|
79
|
+
connection.execute(obj.sql_query)
|
|
53
80
|
|
|
54
81
|
obj.status = :done
|
|
55
82
|
end
|
|
56
83
|
|
|
57
|
-
def create_dependencies(
|
|
58
|
-
dependencies.each { |dep_name| create_object(find_object(dep_name)) }
|
|
84
|
+
def create_dependencies(obj, stack)
|
|
85
|
+
obj.dependencies.each { |dep_name| create_object(find_object(dep_name, obj), stack) }
|
|
59
86
|
end
|
|
60
87
|
|
|
61
|
-
|
|
62
|
-
|
|
88
|
+
# Resolution chain that closed the cycle: from the first occurrence of the
|
|
89
|
+
# revisited object down to the point of revisit, closed with the object
|
|
90
|
+
# itself (e.g. ["a", "b", "a"]).
|
|
91
|
+
def cycle_path(obj, stack)
|
|
92
|
+
first_seen = stack.index(obj.name)
|
|
93
|
+
(first_seen ? stack[first_seen..] : stack) + [obj.name]
|
|
94
|
+
end
|
|
63
95
|
|
|
64
|
-
|
|
65
|
-
|
|
96
|
+
def build_objects_index
|
|
97
|
+
@objects_index = objects.each_with_object({}) do |obj, index|
|
|
98
|
+
[obj.name, obj.full_name, obj.object_name, obj.qualified_object_name].compact.uniq.each do |key|
|
|
99
|
+
(index[key] ||= []) << obj
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def find_object(dep_name, referrer = nil)
|
|
105
|
+
result = @objects_index[dep_name] || []
|
|
106
|
+
|
|
107
|
+
raise ambiguous_error(dep_name, result, referrer) if result.size > 1
|
|
108
|
+
raise PgObjects::DependencyNotExistError.new(dep_name, referrer: referrer&.full_name) if result.empty?
|
|
66
109
|
|
|
67
110
|
result[0]
|
|
68
111
|
end
|
|
112
|
+
|
|
113
|
+
def ambiguous_error(dep_name, result, referrer)
|
|
114
|
+
PgObjects::AmbiguousDependencyError.new(
|
|
115
|
+
dep_name,
|
|
116
|
+
candidates: result.map(&:full_name),
|
|
117
|
+
referrer: referrer&.full_name
|
|
118
|
+
)
|
|
119
|
+
end
|
|
69
120
|
end
|
|
@@ -1,8 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
#
|
|
2
4
|
# AGGREGATE object representation
|
|
3
5
|
#
|
|
4
6
|
class PgObjects::ParsedObject::Aggregate < PgObjects::ParsedObject::Base
|
|
5
7
|
def name
|
|
6
|
-
stmt.define_stmt.defnames
|
|
8
|
+
extract_name { stmt.define_stmt.defnames.last.string.sval }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def schema
|
|
14
|
+
extract_name { qualifier(stmt.define_stmt.defnames) }
|
|
7
15
|
end
|
|
8
16
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
#
|
|
2
4
|
# Base class for parsed objects
|
|
3
5
|
#
|
|
@@ -10,7 +12,43 @@ class PgObjects::ParsedObject::Base
|
|
|
10
12
|
raise NotImplementedError
|
|
11
13
|
end
|
|
12
14
|
|
|
15
|
+
# Schema-qualified name (+schema.name+) when a schema is present, otherwise
|
|
16
|
+
# the same as +name+. Used to disambiguate same-named objects across schemas.
|
|
17
|
+
def qualified_name
|
|
18
|
+
schema_name = schema
|
|
19
|
+
schema_name && !schema_name.empty? ? "#{schema_name}.#{name}" : name
|
|
20
|
+
end
|
|
21
|
+
|
|
13
22
|
private
|
|
14
23
|
|
|
15
24
|
attr_reader :stmt
|
|
25
|
+
|
|
26
|
+
# Schema of the object, or nil when unqualified. Subclasses that carry a
|
|
27
|
+
# schema override this; the rest inherit the unqualified default.
|
|
28
|
+
def schema
|
|
29
|
+
nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Returns the schema element of a pg_query name list (e.g. funcname,
|
|
33
|
+
# defnames) when the name is qualified (+schema.name+), otherwise nil.
|
|
34
|
+
def qualifier(list)
|
|
35
|
+
list[-2].string.sval if list.size > 1
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Wraps name extraction so a malformed statement (nil field, empty array)
|
|
39
|
+
# surfaces an explicit error with context. Only NoMethodErrors from
|
|
40
|
+
# dereferencing nil are wrapped; genuine programming errors propagate.
|
|
41
|
+
def extract_name
|
|
42
|
+
yield
|
|
43
|
+
rescue NoMethodError => e
|
|
44
|
+
raise unless nil_dereference?(e)
|
|
45
|
+
|
|
46
|
+
raise PgObjects::MalformedStatementError, "malformed statement for #{self.class}: #{e.message}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def nil_dereference?(error)
|
|
50
|
+
error.receiver.nil?
|
|
51
|
+
rescue ArgumentError
|
|
52
|
+
false
|
|
53
|
+
end
|
|
16
54
|
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
# Base (scalar) TYPE object representation (CREATE TYPE with I/O functions or
|
|
5
|
+
# a shell CREATE TYPE without attributes) — parsed as a define_stmt.
|
|
6
|
+
#
|
|
7
|
+
class PgObjects::ParsedObject::BaseType < PgObjects::ParsedObject::Base
|
|
8
|
+
def name
|
|
9
|
+
extract_name { stmt.define_stmt.defnames.last.string.sval }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def schema
|
|
15
|
+
extract_name { qualifier(stmt.define_stmt.defnames) }
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -1,8 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
#
|
|
2
4
|
# CONVERSION object representation
|
|
3
5
|
#
|
|
4
6
|
class PgObjects::ParsedObject::Conversion < PgObjects::ParsedObject::Base
|
|
5
7
|
def name
|
|
6
|
-
stmt.create_conversion_stmt.conversion_name
|
|
8
|
+
extract_name { stmt.create_conversion_stmt.conversion_name.last.string.sval }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def schema
|
|
14
|
+
extract_name { qualifier(stmt.create_conversion_stmt.conversion_name) }
|
|
7
15
|
end
|
|
8
16
|
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
# DOMAIN object representation (CREATE DOMAIN)
|
|
5
|
+
#
|
|
6
|
+
class PgObjects::ParsedObject::Domain < PgObjects::ParsedObject::Base
|
|
7
|
+
def name
|
|
8
|
+
extract_name { stmt.create_domain_stmt.domainname.last.string.sval }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def schema
|
|
14
|
+
extract_name { qualifier(stmt.create_domain_stmt.domainname) }
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
# ENUM TYPE object representation (CREATE TYPE ... AS ENUM)
|
|
5
|
+
#
|
|
6
|
+
class PgObjects::ParsedObject::EnumType < PgObjects::ParsedObject::Base
|
|
7
|
+
def name
|
|
8
|
+
extract_name { stmt.create_enum_stmt.type_name.last.string.sval }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def schema
|
|
14
|
+
extract_name { qualifier(stmt.create_enum_stmt.type_name) }
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
#
|
|
2
4
|
# EVENT TRIGGER object representation
|
|
3
5
|
#
|
|
4
6
|
class PgObjects::ParsedObject::EventTrigger < PgObjects::ParsedObject::Base
|
|
5
7
|
def name
|
|
6
|
-
stmt.create_event_trig_stmt.trigname
|
|
8
|
+
extract_name { stmt.create_event_trig_stmt.trigname }
|
|
7
9
|
end
|
|
8
10
|
end
|
|
@@ -1,8 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
#
|
|
2
4
|
# FUNCTION object representation
|
|
3
5
|
#
|
|
4
6
|
class PgObjects::ParsedObject::Function < PgObjects::ParsedObject::Base
|
|
5
7
|
def name
|
|
6
|
-
stmt.create_function_stmt.funcname
|
|
8
|
+
extract_name { stmt.create_function_stmt.funcname.last.string.sval }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def schema
|
|
14
|
+
extract_name { qualifier(stmt.create_function_stmt.funcname) }
|
|
7
15
|
end
|
|
8
16
|
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
# INDEX object representation
|
|
5
|
+
#
|
|
6
|
+
class PgObjects::ParsedObject::Index < PgObjects::ParsedObject::Base
|
|
7
|
+
def name
|
|
8
|
+
extract_name { stmt.index_stmt.idxname }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
# An index lives in the schema of the table it indexes.
|
|
14
|
+
def schema
|
|
15
|
+
extract_name { stmt.index_stmt.relation.schemaname }
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -1,8 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
#
|
|
2
4
|
# MATERIALIZED VIEW object representation
|
|
3
5
|
#
|
|
4
6
|
class PgObjects::ParsedObject::MaterializedView < PgObjects::ParsedObject::Base
|
|
5
7
|
def name
|
|
6
|
-
stmt.create_table_as_stmt.into.rel.relname
|
|
8
|
+
extract_name { stmt.create_table_as_stmt.into.rel.relname }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def schema
|
|
14
|
+
extract_name { stmt.create_table_as_stmt.into.rel.schemaname }
|
|
7
15
|
end
|
|
8
16
|
end
|
|
@@ -1,8 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
#
|
|
2
4
|
# OPERATOR object representation
|
|
3
5
|
#
|
|
4
6
|
class PgObjects::ParsedObject::Operator < PgObjects::ParsedObject::Base
|
|
5
7
|
def name
|
|
6
|
-
stmt.define_stmt.defnames
|
|
8
|
+
extract_name { stmt.define_stmt.defnames.last.string.sval }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def schema
|
|
14
|
+
extract_name { qualifier(stmt.define_stmt.defnames) }
|
|
7
15
|
end
|
|
8
16
|
end
|