pg_objects 1.4.7 → 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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/.evilution.yml +74 -0
  3. data/.github/workflows/ci.yml +1 -1
  4. data/.gitignore +3 -0
  5. data/.rubocop.yml +13 -2
  6. data/CHANGELOG.md +78 -0
  7. data/Gemfile +2 -0
  8. data/Gemfile.lock +96 -78
  9. data/README.md +174 -3
  10. data/Rakefile +2 -0
  11. data/bin/benchmark +42 -7
  12. data/bin/console +1 -0
  13. data/lib/generators/pg_objects/install/install_generator.rb +2 -0
  14. data/lib/pg_objects/config.rb +53 -2
  15. data/lib/pg_objects/db_object.rb +6 -1
  16. data/lib/pg_objects/db_object_factory.rb +11 -3
  17. data/lib/pg_objects/logger.rb +34 -3
  18. data/lib/pg_objects/manager.rb +66 -15
  19. data/lib/pg_objects/parsed_object/aggregate.rb +9 -1
  20. data/lib/pg_objects/parsed_object/base.rb +38 -0
  21. data/lib/pg_objects/parsed_object/base_type.rb +17 -0
  22. data/lib/pg_objects/parsed_object/conversion.rb +9 -1
  23. data/lib/pg_objects/parsed_object/domain.rb +16 -0
  24. data/lib/pg_objects/parsed_object/enum_type.rb +16 -0
  25. data/lib/pg_objects/parsed_object/event_trigger.rb +3 -1
  26. data/lib/pg_objects/parsed_object/extension.rb +10 -0
  27. data/lib/pg_objects/parsed_object/function.rb +9 -1
  28. data/lib/pg_objects/parsed_object/index.rb +17 -0
  29. data/lib/pg_objects/parsed_object/materialized_view.rb +9 -1
  30. data/lib/pg_objects/parsed_object/operator.rb +9 -1
  31. data/lib/pg_objects/parsed_object/operator_class.rb +9 -1
  32. data/lib/pg_objects/parsed_object/policy.rb +10 -0
  33. data/lib/pg_objects/parsed_object/range_type.rb +16 -0
  34. data/lib/pg_objects/parsed_object/rule.rb +10 -0
  35. data/lib/pg_objects/parsed_object/sequence.rb +16 -0
  36. data/lib/pg_objects/parsed_object/table.rb +9 -1
  37. data/lib/pg_objects/parsed_object/text_search_parser.rb +9 -1
  38. data/lib/pg_objects/parsed_object/text_search_template.rb +9 -1
  39. data/lib/pg_objects/parsed_object/trigger.rb +3 -1
  40. data/lib/pg_objects/parsed_object/type.rb +9 -1
  41. data/lib/pg_objects/parsed_object/view.rb +9 -1
  42. data/lib/pg_objects/parsed_object.rb +11 -0
  43. data/lib/pg_objects/parsed_object_factory.rb +52 -90
  44. data/lib/pg_objects/parser.rb +28 -12
  45. data/lib/pg_objects/railtie.rb +2 -0
  46. data/lib/pg_objects/version.rb +3 -1
  47. data/lib/pg_objects/yaml_configurable.rb +25 -7
  48. data/lib/pg_objects.rb +54 -4
  49. data/lib/tasks/pg_objects_tasks.rake +33 -8
  50. data/pg_objects.gemspec +5 -3
  51. metadata +17 -20
  52. data/.github/copilot-instructions.md +0 -131
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # A module that provides a method to load configuration settings from a YAML file.
2
4
  module YamlConfigurable
3
5
  # Loads configuration settings from a YAML file.
@@ -6,17 +8,33 @@ module YamlConfigurable
6
8
  def load_from_yaml(file_path)
7
9
  return unless File.exist?(file_path)
8
10
 
9
- config_hash = YAML.load_file(file_path)
10
-
11
- set_if_present(config, :before_path, config_hash.dig('directories', 'before'))
12
- set_if_present(config, :after_path, config_hash.dig('directories', 'after'))
13
- set_if_present(config, :extensions, config_hash['extensions'])
14
- set_if_present(config, :silent, config_hash['silent'])
11
+ settings_from(YAML.load_file(file_path)).each do |key, value|
12
+ set_if_present(config, key, value)
13
+ end
14
+ rescue Psych::SyntaxError => e
15
+ warn "[pg_objects] Ignoring malformed YAML config #{file_path}: #{e.message}"
15
16
  end
16
17
 
17
18
  private
18
19
 
20
+ # Maps configuration keys to their values in the parsed YAML hash.
21
+ def settings_from(config_hash)
22
+ {
23
+ before_path: config_hash.dig('directories', 'before'),
24
+ after_path: config_hash.dig('directories', 'after'),
25
+ extensions: config_hash['extensions'],
26
+ silent: config_hash['silent'],
27
+ transactional: config_hash['transactional'],
28
+ auto_hook_migrations: config_hash['auto_hook_migrations']
29
+ }
30
+ end
31
+
32
+ # Applies the value unless it is nil or an empty string/array. Booleans
33
+ # (including +false+) have no +empty?+ and are always applied.
19
34
  def set_if_present(config, key, value)
20
- config.public_send("#{key}=", value) if value.present?
35
+ return if value.nil?
36
+ return if value.respond_to?(:empty?) && value.empty?
37
+
38
+ config.public_send("#{key}=", value)
21
39
  end
22
40
  end
data/lib/pg_objects.rb CHANGED
@@ -1,10 +1,61 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'pg_objects/version'
2
4
 
3
5
  module PgObjects
4
- class AmbiguousDependencyError < StandardError; end
5
- class CyclicDependencyError < StandardError; end
6
- class DependencyNotExistError < StandardError; end
6
+ # Raised when a dependency name resolves to more than one object. Carries
7
+ # the matching objects' file paths via +candidates+ and the file that
8
+ # declared the dependency via +referrer+; the message lists them all. Also
9
+ # accepts a bare name for compatibility.
10
+ class AmbiguousDependencyError < StandardError
11
+ attr_reader :candidates, :referrer
12
+
13
+ def initialize(dep_name = nil, candidates: [], referrer: nil)
14
+ @candidates = candidates
15
+ @referrer = referrer
16
+ message = build_message(dep_name)
17
+ message.empty? ? super() : super(message)
18
+ end
19
+
20
+ private
21
+
22
+ def build_message(dep_name)
23
+ message = dep_name.to_s
24
+ message += " (referenced by #{referrer})" if referrer
25
+ message += " matches: #{candidates.join(', ')}" unless candidates.empty?
26
+ message
27
+ end
28
+ end
29
+
30
+ # Raised when object dependencies form a cycle. Carries the resolution chain
31
+ # that closed the cycle (e.g. ["a", "b", "a"]) via +cycle_path+; the message
32
+ # renders it as "a -> b -> a". Also accepts a single name for compatibility.
33
+ class CyclicDependencyError < StandardError
34
+ attr_reader :cycle_path
35
+
36
+ def initialize(cycle_path = nil)
37
+ @cycle_path = Array(cycle_path)
38
+ @cycle_path.empty? ? super() : super(@cycle_path.join(' -> '))
39
+ end
40
+ end
41
+
42
+ # Raised when a declared dependency matches no loaded object. Carries the
43
+ # file that declared the dependency via +referrer+ and includes it in the
44
+ # message. Also accepts a bare name for compatibility.
45
+ class DependencyNotExistError < StandardError
46
+ attr_reader :referrer
47
+
48
+ def initialize(dep_name = nil, referrer: nil)
49
+ @referrer = referrer
50
+ message = dep_name.to_s
51
+ message += " (referenced by #{referrer})" unless referrer.to_s.empty?
52
+ message.empty? ? super() : super(message)
53
+ end
54
+ end
55
+
7
56
  class UnsupportedAdapterError < StandardError; end
57
+ class UnknownObjectTypeError < StandardError; end
58
+ class MalformedStatementError < StandardError; end
8
59
  end
9
60
 
10
61
  require 'pg_objects/railtie' if defined?(Rails)
@@ -12,7 +63,6 @@ require 'pg_objects/railtie' if defined?(Rails)
12
63
  require 'dry-configurable'
13
64
  require 'dry-container'
14
65
  require 'dry-auto_inject'
15
- require 'dry/monads'
16
66
  require 'memery'
17
67
 
18
68
  require 'pg_objects/container'
@@ -1,23 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Resolves the connection to run against. Set PG_OBJECTS_CONNECTION_CLASS to
4
+ # the name of an Active Record class (e.g. "AnimalsRecord") to target that
5
+ # class's database in a multi-DB setup; unset, the global connection is used.
6
+ pg_objects_connection = lambda do
7
+ class_name = ENV.fetch('PG_OBJECTS_CONNECTION_CLASS', nil)
8
+ next nil if class_name.nil? || class_name.strip.empty?
9
+
10
+ begin
11
+ Object.const_get(class_name).connection
12
+ rescue NameError
13
+ raise ArgumentError, "PG_OBJECTS_CONNECTION_CLASS is set to unknown class #{class_name.inspect}"
14
+ end
15
+ end
16
+
1
17
  namespace :db do
2
18
  namespace :create_objects do
3
19
  desc 'Create all the database objects from "before" folder'
4
20
  task before: :environment do
5
- PgObjects::Manager.new.load_files(:before).create_objects
21
+ PgObjects::Manager.new(connection: pg_objects_connection.call).load_files(:before).create_objects
6
22
  end
7
23
 
8
24
  desc 'Create all the database objects from "after" folder'
9
25
  task after: :environment do
10
- PgObjects::Manager.new.load_files(:after).create_objects
26
+ PgObjects::Manager.new(connection: pg_objects_connection.call).load_files(:after).create_objects
11
27
  end
12
28
  end
13
29
  end
14
30
 
15
31
  require 'rake/hooks'
16
32
 
17
- before 'db:migrate' do
18
- Rake::Task['db:create_objects:before'].invoke
19
- end
20
-
21
- after 'db:migrate' do
22
- Rake::Task['db:create_objects:after'].invoke
33
+ # Attach object-creation hooks to the tasks configured in
34
+ # PgObjects::Config.config.hook_tasks (override in an initializer to opt out).
35
+ # Each stage (:before/:after) maps to both the rake-hooks DSL method and the
36
+ # matching db:create_objects:<stage> task. Set config.auto_hook_migrations to
37
+ # false to install no hooks at all.
38
+ if PgObjects::Config.config.auto_hook_migrations
39
+ PgObjects::Config.config.hook_tasks.each do |task_name, stages|
40
+ stages.each do |stage|
41
+ send(stage, task_name) do
42
+ task = Rake::Task["db:create_objects:#{stage}"]
43
+ task.reenable # allow the hook to run again within composed/repeated task runs
44
+ task.invoke
45
+ end
46
+ end
47
+ end
23
48
  end
data/pg_objects.gemspec CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  lib = File.expand_path('lib', __dir__)
2
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
5
 
@@ -12,11 +14,12 @@ Gem::Specification.new do |spec|
12
14
  spec.summary = %q(Simple manager for PostgreSQL objects like triggers and functions)
13
15
  spec.homepage = 'https://github.com/marinazzio/pg_objects'
14
16
 
15
- spec.required_ruby_version = '>= 3.2.0'
17
+ spec.required_ruby_version = '>= 3.3.0'
16
18
 
17
19
  spec.metadata = {
18
20
  'allowed_push_host' => 'https://rubygems.org',
19
21
  'bug_tracker_uri' => 'https://github.com/marinazzio/pg_objects/issues',
22
+ 'changelog_uri' => 'https://github.com/marinazzio/pg_objects/blob/master/CHANGELOG.md',
20
23
  'documentation_uri' => 'https://github.com/marinazzio/pg_objects/blob/master/README.md',
21
24
  'homepage_uri' => 'https://github.com/marinazzio/pg_objects',
22
25
  'rubygems_mfa_required' => 'true',
@@ -46,8 +49,7 @@ Gem::Specification.new do |spec|
46
49
  spec.add_dependency 'activerecord', '>= 6.1.7.0', '< 9'
47
50
  spec.add_dependency 'dry-auto_inject', '~> 1'
48
51
  spec.add_dependency 'dry-configurable', '~> 1'
49
- spec.add_dependency 'dry-container', '0.11.0'
50
- spec.add_dependency 'dry-monads', '~> 1.6'
52
+ spec.add_dependency 'dry-container', '~> 0.11'
51
53
  spec.add_dependency 'memery', '>= 1.5', '< 1.9'
52
54
  spec.add_dependency 'pg_query', '>= 5', '< 7'
53
55
  spec.add_dependency 'railties', '>= 4', '< 9'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pg_objects
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.7
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Denis Kiselyov
@@ -59,32 +59,18 @@ dependencies:
59
59
  version: '1'
60
60
  - !ruby/object:Gem::Dependency
61
61
  name: dry-container
62
- requirement: !ruby/object:Gem::Requirement
63
- requirements:
64
- - - '='
65
- - !ruby/object:Gem::Version
66
- version: 0.11.0
67
- type: :runtime
68
- prerelease: false
69
- version_requirements: !ruby/object:Gem::Requirement
70
- requirements:
71
- - - '='
72
- - !ruby/object:Gem::Version
73
- version: 0.11.0
74
- - !ruby/object:Gem::Dependency
75
- name: dry-monads
76
62
  requirement: !ruby/object:Gem::Requirement
77
63
  requirements:
78
64
  - - "~>"
79
65
  - !ruby/object:Gem::Version
80
- version: '1.6'
66
+ version: '0.11'
81
67
  type: :runtime
82
68
  prerelease: false
83
69
  version_requirements: !ruby/object:Gem::Requirement
84
70
  requirements:
85
71
  - - "~>"
86
72
  - !ruby/object:Gem::Version
87
- version: '1.6'
73
+ version: '0.11'
88
74
  - !ruby/object:Gem::Dependency
89
75
  name: memery
90
76
  requirement: !ruby/object:Gem::Requirement
@@ -165,7 +151,7 @@ executables: []
165
151
  extensions: []
166
152
  extra_rdoc_files: []
167
153
  files:
168
- - ".github/copilot-instructions.md"
154
+ - ".evilution.yml"
169
155
  - ".github/dependabot.yml"
170
156
  - ".github/workflows/bundle_audit.yml"
171
157
  - ".github/workflows/ci.yml"
@@ -174,6 +160,7 @@ files:
174
160
  - ".rspec"
175
161
  - ".rubocop.yml"
176
162
  - ".rubocop_todo.yml"
163
+ - CHANGELOG.md
177
164
  - Gemfile
178
165
  - Gemfile.lock
179
166
  - LICENSE
@@ -195,12 +182,21 @@ files:
195
182
  - lib/pg_objects/parsed_object.rb
196
183
  - lib/pg_objects/parsed_object/aggregate.rb
197
184
  - lib/pg_objects/parsed_object/base.rb
185
+ - lib/pg_objects/parsed_object/base_type.rb
198
186
  - lib/pg_objects/parsed_object/conversion.rb
187
+ - lib/pg_objects/parsed_object/domain.rb
188
+ - lib/pg_objects/parsed_object/enum_type.rb
199
189
  - lib/pg_objects/parsed_object/event_trigger.rb
190
+ - lib/pg_objects/parsed_object/extension.rb
200
191
  - lib/pg_objects/parsed_object/function.rb
192
+ - lib/pg_objects/parsed_object/index.rb
201
193
  - lib/pg_objects/parsed_object/materialized_view.rb
202
194
  - lib/pg_objects/parsed_object/operator.rb
203
195
  - lib/pg_objects/parsed_object/operator_class.rb
196
+ - lib/pg_objects/parsed_object/policy.rb
197
+ - lib/pg_objects/parsed_object/range_type.rb
198
+ - lib/pg_objects/parsed_object/rule.rb
199
+ - lib/pg_objects/parsed_object/sequence.rb
204
200
  - lib/pg_objects/parsed_object/table.rb
205
201
  - lib/pg_objects/parsed_object/text_search_parser.rb
206
202
  - lib/pg_objects/parsed_object/text_search_template.rb
@@ -220,6 +216,7 @@ licenses:
220
216
  metadata:
221
217
  allowed_push_host: https://rubygems.org
222
218
  bug_tracker_uri: https://github.com/marinazzio/pg_objects/issues
219
+ changelog_uri: https://github.com/marinazzio/pg_objects/blob/master/CHANGELOG.md
223
220
  documentation_uri: https://github.com/marinazzio/pg_objects/blob/master/README.md
224
221
  homepage_uri: https://github.com/marinazzio/pg_objects
225
222
  rubygems_mfa_required: 'true'
@@ -236,14 +233,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
236
233
  requirements:
237
234
  - - ">="
238
235
  - !ruby/object:Gem::Version
239
- version: 3.2.0
236
+ version: 3.3.0
240
237
  required_rubygems_version: !ruby/object:Gem::Requirement
241
238
  requirements:
242
239
  - - ">="
243
240
  - !ruby/object:Gem::Version
244
241
  version: '0'
245
242
  requirements: []
246
- rubygems_version: 4.0.3
243
+ rubygems_version: 4.0.16
247
244
  specification_version: 4
248
245
  summary: Simple manager for PostgreSQL objects like triggers and functions
249
246
  test_files: []
@@ -1,131 +0,0 @@
1
- # PG Objects
2
- PG Objects is a Ruby gem for managing PostgreSQL database objects like triggers and functions. It provides a simple manager that handles dependencies between database objects and integrates with Rails applications.
3
-
4
- Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.
5
-
6
- ## Working Effectively
7
- - Bootstrap, build, and test the repository:
8
- - `gem install --user-install bundler`
9
- - `export PATH="$HOME/.local/share/gem/ruby/3.2.0/bin:$PATH"`
10
- - `bundle config set --local path 'vendor/bundle'`
11
- - `bundle install` -- takes 45-60 seconds. NEVER CANCEL. Set timeout to 120+ seconds.
12
- - `bundle exec rspec spec` -- takes 4 seconds. NEVER CANCEL. Set timeout to 60+ seconds.
13
- - `bundle exec rubocop` -- takes 3 seconds. NEVER CANCEL. Set timeout to 60+ seconds.
14
- - `bundle exec bundle-audit check` -- takes 2 seconds (first run downloads advisory database). NEVER CANCEL. Set timeout to 180+ seconds for first run.
15
- - Performance benchmarking:
16
- - `bundle exec rake benchmark` -- takes 1 second. NEVER CANCEL. Set timeout to 30+ seconds.
17
- - Interactive console:
18
- - `./bin/console` -- launches IRB with pg_objects loaded
19
- - Install gem locally:
20
- - `bundle exec rake install` -- takes 9 seconds. NEVER CANCEL. Set timeout to 120+ seconds.
21
-
22
- ## Validation
23
- - Always run through the complete test suite after making changes: `bundle exec rspec spec`
24
- - ALWAYS run linting before completing work: `bundle exec rubocop`
25
- - Always run bundle audit to check for security vulnerabilities: `bundle exec bundle-audit check`
26
- - Test parsing functionality with sample SQL files to ensure changes work correctly
27
- - NEVER CANCEL builds or tests - they complete quickly (under 60 seconds)
28
-
29
- ## Common Tasks
30
- The following are outputs from frequently run commands. Reference them instead of viewing, searching, or running bash commands to save time.
31
-
32
- ### Repository Root Structure
33
- ```
34
- .
35
- ├── .github/ # CI/CD workflows (ci.yml, bundle_audit.yml, publish.yml)
36
- ├── .rspec # RSpec configuration
37
- ├── .rubocop.yml # RuboCop linting configuration
38
- ├── bin/
39
- │ ├── setup # Setup script (runs bundle install)
40
- │ ├── console # Interactive console
41
- │ └── benchmark # Performance benchmark tool
42
- ├── lib/
43
- │ ├── pg_objects.rb # Main entry point
44
- │ ├── pg_objects/ # Core library files
45
- │ │ ├── config.rb
46
- │ │ ├── manager.rb
47
- │ │ ├── parser.rb
48
- │ │ └── parsed_object/ # SQL object parsers
49
- │ └── generators/pg_objects/install/ # Rails generator
50
- ├── spec/ # RSpec test files
51
- ├── Gemfile # Gem dependencies
52
- ├── pg_objects.gemspec # Gem specification
53
- ├── Rakefile # Rake tasks (spec, benchmark)
54
- └── README.md # Documentation
55
- ```
56
-
57
- ### Key Files and Directories
58
- - **lib/pg_objects.rb**: Main entry point that requires all components
59
- - **lib/pg_objects/manager.rb**: Core manager for database objects
60
- - **lib/pg_objects/parser.rb**: SQL parsing and dependency extraction
61
- - **lib/pg_objects/parsed_object/**: Specific parsers for different SQL object types
62
- - **spec/**: Complete test suite with fixtures
63
- - **bin/benchmark**: Performance benchmarking tool with detailed metrics
64
- - **Gemfile**: Development and test dependencies (RSpec, RuboCop, etc.)
65
-
66
- ### Gemfile Dependencies
67
- - **Runtime**: activerecord, dry-auto_inject, dry-configurable, pg_query, railties
68
- - **Development/Test**: rspec, rubocop, bundler-audit, faker, pry-byebug
69
-
70
- ### Common Command Outputs
71
- #### `bundle exec rspec spec` (Expected: ~4 seconds, 54 examples, 0 failures)
72
- ```
73
- 54 examples, 0 failures
74
- Finished in 2.24 seconds
75
- ```
76
-
77
- #### `bundle exec rubocop` (Expected: ~3 seconds, 60 files, no offenses)
78
- ```
79
- 60 files inspected, no offenses detected
80
- ```
81
-
82
- #### `bundle exec bundle-audit check` (Expected: ~2 seconds after initial setup)
83
- ```
84
- No vulnerabilities found
85
- ```
86
-
87
- #### `bundle exec rake benchmark` (Expected: ~1 second)
88
- ```
89
- PG Objects Performance Benchmark
90
- ==================================================
91
- File I/O Performance: ~100,000+ files/s
92
- Parsing Performance: ~7,000 files/s
93
- Full Workflow Performance: ~6,000 objects/s
94
- Benchmark completed successfully!
95
- ```
96
-
97
- ## Development Workflow
98
- 1. Always run `bundle install` after cloning or changing dependencies
99
- 2. Make changes to code in lib/ directory
100
- 3. Add or update tests in spec/ directory for any changes
101
- 4. Run `bundle exec rspec spec` to ensure all tests pass
102
- 5. Run `bundle exec rubocop` to ensure code style compliance
103
- 6. Use `bundle exec rake benchmark` to test performance impact
104
- 7. Run `bundle exec bundle-audit check` for security validation
105
-
106
- ## Troubleshooting
107
- - If bundler is not found: `gem install --user-install bundler && export PATH="$HOME/.local/share/gem/ruby/3.2.0/bin:$PATH"`
108
- - If bundle install fails with permission errors: `bundle config set --local path 'vendor/bundle'`
109
- - Ruby version required: >= 3.2.0 (tested with 3.2, 3.3, 3.4)
110
- - The gem requires PostgreSQL and uses pg_query for SQL parsing
111
- - Dependencies include ActiveRecord, dry gems, and memery for caching
112
-
113
- ## Testing SQL Parsing
114
- Create test SQL files with dependencies:
115
- ```sql
116
- --!depends_on other_function
117
- CREATE OR REPLACE FUNCTION my_function(param INTEGER)
118
- RETURNS INTEGER AS $$
119
- BEGIN
120
- RETURN param * 2;
121
- END;
122
- $$ LANGUAGE plpgsql;
123
- ```
124
-
125
- Test parsing with:
126
- ```ruby
127
- parser = PgObjects::Parser.new
128
- content = File.read('path/to/file.sql')
129
- object_name = parser.load(content).fetch_object_name
130
- dependencies = parser.fetch_directives[:depends_on]
131
- ```