pg_trunk 0.1.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 +7 -0
- data/.github/workflows/ci.yml +87 -0
- data/.gitignore +9 -0
- data/.rspec +4 -0
- data/.rubocop.yml +92 -0
- data/.yardopts +4 -0
- data/CHANGELOG.md +31 -0
- data/CONTRIBUTING.md +17 -0
- data/Gemfile +22 -0
- data/LICENSE.txt +21 -0
- data/README.md +141 -0
- data/Rakefile +16 -0
- data/bin/console +8 -0
- data/bin/rake +19 -0
- data/bin/rspec +19 -0
- data/bin/setup +8 -0
- data/bin/yard +19 -0
- data/lib/pg_trunk/core/adapters/postgres.rb +80 -0
- data/lib/pg_trunk/core/dependencies_resolver.rb +101 -0
- data/lib/pg_trunk/core/generators.rb +140 -0
- data/lib/pg_trunk/core/operation/attributes.rb +78 -0
- data/lib/pg_trunk/core/operation/callbacks.rb +40 -0
- data/lib/pg_trunk/core/operation/generators.rb +51 -0
- data/lib/pg_trunk/core/operation/inversion.rb +70 -0
- data/lib/pg_trunk/core/operation/registration.rb +55 -0
- data/lib/pg_trunk/core/operation/ruby_builder.rb +112 -0
- data/lib/pg_trunk/core/operation/ruby_helpers.rb +99 -0
- data/lib/pg_trunk/core/operation/sql_helpers.rb +44 -0
- data/lib/pg_trunk/core/operation/validations.rb +21 -0
- data/lib/pg_trunk/core/operation.rb +78 -0
- data/lib/pg_trunk/core/qualified_name.rb +165 -0
- data/lib/pg_trunk/core/railtie/command_recorder.rb +30 -0
- data/lib/pg_trunk/core/railtie/custom_types.rb +37 -0
- data/lib/pg_trunk/core/railtie/migration.rb +50 -0
- data/lib/pg_trunk/core/railtie/migrator.rb +22 -0
- data/lib/pg_trunk/core/railtie/schema_dumper.rb +75 -0
- data/lib/pg_trunk/core/railtie/schema_migration.rb +22 -0
- data/lib/pg_trunk/core/railtie/statements.rb +21 -0
- data/lib/pg_trunk/core/railtie.rb +35 -0
- data/lib/pg_trunk/core/registry.rb +159 -0
- data/lib/pg_trunk/core/serializers/array_of_hashes_serializer.rb +28 -0
- data/lib/pg_trunk/core/serializers/array_of_strings_serializer.rb +29 -0
- data/lib/pg_trunk/core/serializers/array_of_symbols_serializer.rb +28 -0
- data/lib/pg_trunk/core/serializers/array_serializer.rb +22 -0
- data/lib/pg_trunk/core/serializers/lowercase_string_serializer.rb +21 -0
- data/lib/pg_trunk/core/serializers/multiline_text_serializer.rb +21 -0
- data/lib/pg_trunk/core/serializers/qualified_name_serializer.rb +27 -0
- data/lib/pg_trunk/core/serializers/symbol_serializer.rb +22 -0
- data/lib/pg_trunk/core/serializers.rb +16 -0
- data/lib/pg_trunk/core/validators/all_items_valid_validator.rb +15 -0
- data/lib/pg_trunk/core/validators/difference_validator.rb +19 -0
- data/lib/pg_trunk/core/validators.rb +10 -0
- data/lib/pg_trunk/core.rb +21 -0
- data/lib/pg_trunk/generators.rb +7 -0
- data/lib/pg_trunk/operations/check_constraints/add_check_constraint.rb +109 -0
- data/lib/pg_trunk/operations/check_constraints/base.rb +69 -0
- data/lib/pg_trunk/operations/check_constraints/drop_check_constraint.rb +60 -0
- data/lib/pg_trunk/operations/check_constraints/rename_check_constraint.rb +54 -0
- data/lib/pg_trunk/operations/check_constraints/validate_check_constraint.rb +39 -0
- data/lib/pg_trunk/operations/check_constraints.rb +14 -0
- data/lib/pg_trunk/operations/composite_types/base.rb +61 -0
- data/lib/pg_trunk/operations/composite_types/change_composite_type.rb +136 -0
- data/lib/pg_trunk/operations/composite_types/column.rb +118 -0
- data/lib/pg_trunk/operations/composite_types/create_composite_type.rb +99 -0
- data/lib/pg_trunk/operations/composite_types/drop_composite_type.rb +67 -0
- data/lib/pg_trunk/operations/composite_types/rename_composite_type.rb +44 -0
- data/lib/pg_trunk/operations/composite_types.rb +15 -0
- data/lib/pg_trunk/operations/domains/base.rb +46 -0
- data/lib/pg_trunk/operations/domains/change_domain.rb +140 -0
- data/lib/pg_trunk/operations/domains/constraint.rb +93 -0
- data/lib/pg_trunk/operations/domains/create_domain.rb +124 -0
- data/lib/pg_trunk/operations/domains/drop_domain.rb +65 -0
- data/lib/pg_trunk/operations/domains/rename_domain.rb +44 -0
- data/lib/pg_trunk/operations/domains.rb +15 -0
- data/lib/pg_trunk/operations/enums/base.rb +47 -0
- data/lib/pg_trunk/operations/enums/change.rb +55 -0
- data/lib/pg_trunk/operations/enums/change_enum.rb +119 -0
- data/lib/pg_trunk/operations/enums/create_enum.rb +83 -0
- data/lib/pg_trunk/operations/enums/drop_enum.rb +63 -0
- data/lib/pg_trunk/operations/enums/rename_enum.rb +44 -0
- data/lib/pg_trunk/operations/enums.rb +15 -0
- data/lib/pg_trunk/operations/foreign_keys/add_foreign_key.rb +174 -0
- data/lib/pg_trunk/operations/foreign_keys/base.rb +155 -0
- data/lib/pg_trunk/operations/foreign_keys/drop_foreign_key.rb +76 -0
- data/lib/pg_trunk/operations/foreign_keys/rename_foreign_key.rb +63 -0
- data/lib/pg_trunk/operations/foreign_keys.rb +16 -0
- data/lib/pg_trunk/operations/functions/base.rb +54 -0
- data/lib/pg_trunk/operations/functions/change_function.rb +108 -0
- data/lib/pg_trunk/operations/functions/create_function.rb +198 -0
- data/lib/pg_trunk/operations/functions/drop_function.rb +88 -0
- data/lib/pg_trunk/operations/functions/rename_function.rb +57 -0
- data/lib/pg_trunk/operations/functions.rb +14 -0
- data/lib/pg_trunk/operations/indexes/add_index.rb +68 -0
- data/lib/pg_trunk/operations/indexes.rb +10 -0
- data/lib/pg_trunk/operations/materialized_views/base.rb +79 -0
- data/lib/pg_trunk/operations/materialized_views/change_materialized_view.rb +139 -0
- data/lib/pg_trunk/operations/materialized_views/column.rb +94 -0
- data/lib/pg_trunk/operations/materialized_views/create_materialized_view.rb +170 -0
- data/lib/pg_trunk/operations/materialized_views/drop_materialized_view.rb +70 -0
- data/lib/pg_trunk/operations/materialized_views/refresh_materialized_view.rb +48 -0
- data/lib/pg_trunk/operations/materialized_views/rename_materialized_view.rb +61 -0
- data/lib/pg_trunk/operations/materialized_views.rb +17 -0
- data/lib/pg_trunk/operations/procedures/base.rb +42 -0
- data/lib/pg_trunk/operations/procedures/change_procedure.rb +107 -0
- data/lib/pg_trunk/operations/procedures/create_procedure.rb +146 -0
- data/lib/pg_trunk/operations/procedures/drop_procedure.rb +66 -0
- data/lib/pg_trunk/operations/procedures/rename_procedure.rb +57 -0
- data/lib/pg_trunk/operations/procedures.rb +14 -0
- data/lib/pg_trunk/operations/statistics/base.rb +94 -0
- data/lib/pg_trunk/operations/statistics/create_statistics.rb +181 -0
- data/lib/pg_trunk/operations/statistics/drop_statistics.rb +75 -0
- data/lib/pg_trunk/operations/statistics/rename_statistics.rb +48 -0
- data/lib/pg_trunk/operations/statistics.rb +13 -0
- data/lib/pg_trunk/operations/tables/create_table.rb +75 -0
- data/lib/pg_trunk/operations/tables.rb +10 -0
- data/lib/pg_trunk/operations/triggers/base.rb +119 -0
- data/lib/pg_trunk/operations/triggers/change_trigger.rb +82 -0
- data/lib/pg_trunk/operations/triggers/create_trigger.rb +208 -0
- data/lib/pg_trunk/operations/triggers/drop_trigger.rb +66 -0
- data/lib/pg_trunk/operations/triggers/rename_trigger.rb +71 -0
- data/lib/pg_trunk/operations/triggers.rb +14 -0
- data/lib/pg_trunk/operations/views/base.rb +38 -0
- data/lib/pg_trunk/operations/views/change_view.rb +90 -0
- data/lib/pg_trunk/operations/views/create_view.rb +115 -0
- data/lib/pg_trunk/operations/views/drop_view.rb +69 -0
- data/lib/pg_trunk/operations/views/rename_view.rb +58 -0
- data/lib/pg_trunk/operations/views.rb +14 -0
- data/lib/pg_trunk/operations.rb +23 -0
- data/lib/pg_trunk/version.rb +6 -0
- data/lib/pg_trunk.rb +27 -0
- data/pg_trunk.gemspec +34 -0
- data/spec/dummy/.gitignore +16 -0
- data/spec/dummy/Rakefile +15 -0
- data/spec/dummy/bin/bundle +6 -0
- data/spec/dummy/bin/rails +6 -0
- data/spec/dummy/bin/rake +6 -0
- data/spec/dummy/config/application.rb +18 -0
- data/spec/dummy/config/boot.rb +7 -0
- data/spec/dummy/config/database.yml +14 -0
- data/spec/dummy/config/environment.rb +7 -0
- data/spec/dummy/config.ru +6 -0
- data/spec/dummy/db/materialized_views/admin_users_v01.sql +1 -0
- data/spec/dummy/db/migrate/.keep +0 -0
- data/spec/dummy/db/schema.rb +18 -0
- data/spec/dummy/db/views/admin_users_v01.sql +1 -0
- data/spec/dummy/db/views/admin_users_v02.sql +1 -0
- data/spec/operations/check_constraints/add_check_constraint_spec.rb +85 -0
- data/spec/operations/check_constraints/drop_check_constraint_spec.rb +111 -0
- data/spec/operations/check_constraints/rename_check_constraint_spec.rb +90 -0
- data/spec/operations/composite_types/change_composite_type_spec.rb +257 -0
- data/spec/operations/composite_types/create_composite_type_spec.rb +55 -0
- data/spec/operations/composite_types/drop_composite_type_spec.rb +109 -0
- data/spec/operations/composite_types/rename_composite_type_spec.rb +74 -0
- data/spec/operations/dependency_resolver_spec.rb +177 -0
- data/spec/operations/domains/change_domain_spec.rb +287 -0
- data/spec/operations/domains/create_domain_spec.rb +69 -0
- data/spec/operations/domains/drop_domain_spec.rb +119 -0
- data/spec/operations/domains/rename_domain_spec.rb +70 -0
- data/spec/operations/enums/change_enum_spec.rb +157 -0
- data/spec/operations/enums/create_enum_spec.rb +40 -0
- data/spec/operations/enums/drop_enum_spec.rb +120 -0
- data/spec/operations/enums/rename_enum_spec.rb +72 -0
- data/spec/operations/foreign_keys/add_foreign_key_spec.rb +208 -0
- data/spec/operations/foreign_keys/drop_foreign_key_spec.rb +167 -0
- data/spec/operations/foreign_keys/rename_foreign_key_spec.rb +101 -0
- data/spec/operations/functions/change_function_spec.rb +166 -0
- data/spec/operations/functions/create_function_spec.rb +192 -0
- data/spec/operations/functions/drop_function_spec.rb +182 -0
- data/spec/operations/functions/rename_function_spec.rb +101 -0
- data/spec/operations/indexes/add_index_spec.rb +94 -0
- data/spec/operations/materialized_views/change_materialized_view_spec.rb +190 -0
- data/spec/operations/materialized_views/create_materialized_view_spec.rb +144 -0
- data/spec/operations/materialized_views/drop_materialized_view_spec.rb +145 -0
- data/spec/operations/materialized_views/refresh_materialized_view_spec.rb +79 -0
- data/spec/operations/materialized_views/rename_materialized_view_spec.rb +88 -0
- data/spec/operations/procedures/change_procedure_spec.rb +175 -0
- data/spec/operations/procedures/create_procedure_spec.rb +151 -0
- data/spec/operations/procedures/drop_procedure_spec.rb +159 -0
- data/spec/operations/procedures/rename_procedure_spec.rb +107 -0
- data/spec/operations/statistics/create_statistics_spec.rb +230 -0
- data/spec/operations/statistics/drop_statistics_spec.rb +106 -0
- data/spec/operations/statistics/rename_statistics_spec.rb +129 -0
- data/spec/operations/tables/create_table_spec.rb +53 -0
- data/spec/operations/tables/rename_table_spec.rb +37 -0
- data/spec/operations/triggers/change_trigger_spec.rb +195 -0
- data/spec/operations/triggers/create_trigger_spec.rb +104 -0
- data/spec/operations/triggers/drop_trigger_spec.rb +124 -0
- data/spec/operations/triggers/rename_trigger_spec.rb +160 -0
- data/spec/operations/views/change_view_spec.rb +144 -0
- data/spec/operations/views/create_view_spec.rb +134 -0
- data/spec/operations/views/drop_view_spec.rb +146 -0
- data/spec/operations/views/rename_view_spec.rb +85 -0
- data/spec/pg_trunk/dependencies_resolver_spec.rb +43 -0
- data/spec/spec_helper.rb +28 -0
- data/spec/support/migrations_helper.rb +376 -0
- metadata +348 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PGTrunk
|
|
4
|
+
# @private
|
|
5
|
+
# Resolve dependencies between inter-dependent objects,
|
|
6
|
+
# identified by database `#oid` and comparable to each other.
|
|
7
|
+
#
|
|
8
|
+
# The method builds the sorted list:
|
|
9
|
+
# - parent objects moved before their dependants.
|
|
10
|
+
# - independent objects keeps their original order.
|
|
11
|
+
#
|
|
12
|
+
# We have no expectations about the natural order here.
|
|
13
|
+
# @see [PGTrunk::ObjectDefinitions::Operation]
|
|
14
|
+
class DependenciesResolver
|
|
15
|
+
class << self
|
|
16
|
+
# Resolve dependencies between objects
|
|
17
|
+
# @param [Array<Enumerable, #oid>] objects The list of objects
|
|
18
|
+
# @return [Array<#oid>] The sorted list of objects
|
|
19
|
+
# @raise [Dependencies::CycleError] if dependencies contain a cycle
|
|
20
|
+
def resolve(objects)
|
|
21
|
+
new(objects, dependencies).send(:sorted)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def query
|
|
27
|
+
<<~SQL.squish
|
|
28
|
+
SELECT child, array_agg(parent) AS parents
|
|
29
|
+
FROM (
|
|
30
|
+
SELECT
|
|
31
|
+
d.objid AS child,
|
|
32
|
+
d.refobjid AS parent
|
|
33
|
+
FROM pg_depend d
|
|
34
|
+
JOIN pg_trunk e1 ON d.objid = e1.oid
|
|
35
|
+
JOIN pg_trunk e2 ON d.refobjid = e2.oid
|
|
36
|
+
WHERE d.objsubid IS NULL
|
|
37
|
+
) dependencies
|
|
38
|
+
GROUP BY child;
|
|
39
|
+
SQL
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Extract dependencies between given oids from the database
|
|
43
|
+
def dependencies
|
|
44
|
+
ActiveRecord::Base
|
|
45
|
+
.connection
|
|
46
|
+
.execute(query)
|
|
47
|
+
.each_with_object({}) do |i, obj|
|
|
48
|
+
child = i["child"].to_i
|
|
49
|
+
parents = i["parents"].scan(/\d+/).map(&:to_i)
|
|
50
|
+
obj[child] = parents.uniq
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
# @param [Array<Comparable, #oid>] objects Objects to sort
|
|
58
|
+
# @param [Hash<{Integer => Array<Integer>}] parents Dependencies between oid-s
|
|
59
|
+
def initialize(objects, parents)
|
|
60
|
+
@objects = objects
|
|
61
|
+
@parents = parents.transform_values do |oids|
|
|
62
|
+
# preserve the original order of objects
|
|
63
|
+
objects.each_with_object([]) do |obj, list|
|
|
64
|
+
list << obj if oids.include?(obj.oid)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
attr_reader :objects, :parents
|
|
70
|
+
|
|
71
|
+
def visited
|
|
72
|
+
@visited ||= {}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def index
|
|
76
|
+
@index ||= objects.each_with_object({}) { |obj, idx| idx[obj.oid] = obj }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Use topological sorting algorithm to resolve dependencies
|
|
80
|
+
# @return [Array<#oid>]
|
|
81
|
+
def sorted
|
|
82
|
+
@sorted ||= [].tap do |output|
|
|
83
|
+
while (object = next_unvisited)
|
|
84
|
+
visit(object, output)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def next_unvisited
|
|
90
|
+
objects.find { |object| !visited[object.oid] }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def visit(object, output)
|
|
94
|
+
return if visited[object.oid]
|
|
95
|
+
|
|
96
|
+
parents[object.oid]&.each { |parent| visit(parent, output) }
|
|
97
|
+
visited[object.oid] = true
|
|
98
|
+
output << object
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: false
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/active_record"
|
|
5
|
+
|
|
6
|
+
# @private
|
|
7
|
+
# @abstract
|
|
8
|
+
# Module to build object-specific generators
|
|
9
|
+
module PGTrunk::Generators
|
|
10
|
+
extend ActiveSupport::Concern
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
# Add new generator for given operation
|
|
14
|
+
# @param [Class < PGTrunk::Operation] operation
|
|
15
|
+
def register(operation)
|
|
16
|
+
klass = build_generator(operation)
|
|
17
|
+
class_name = klass.name.split("::").last.to_sym
|
|
18
|
+
remove_const(class_name) if const_defined?(class_name)
|
|
19
|
+
const_set(class_name, klass)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# rubocop: disable Metrics/MethodLength
|
|
23
|
+
def build_generator(operation)
|
|
24
|
+
Class.new(Rails::Generators::NamedBase) do
|
|
25
|
+
include Rails::Generators::Migration
|
|
26
|
+
include PGTrunk::Generators
|
|
27
|
+
|
|
28
|
+
# Add the same arguments as in ruby method
|
|
29
|
+
operation.ruby_params.each do |name|
|
|
30
|
+
argument(name, **operation.attributes[name])
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Add the same options as in ruby method
|
|
34
|
+
operation.attributes.except(*operation.ruby_params).each do |name, opts|
|
|
35
|
+
class_option(name, **opts)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The only command of the generator is to create a migration file
|
|
39
|
+
create_command(:create_migration_file)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
# rubocop: enable Metrics/MethodLength
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class_methods do
|
|
46
|
+
# @!attribute [r] fetcher The module including +PGTrunk::BaseFetcher+
|
|
47
|
+
attr_accessor :operation
|
|
48
|
+
|
|
49
|
+
# The name of the generated object like `foreign_key`
|
|
50
|
+
# for the `add_foreign_key` operation so that the command
|
|
51
|
+
#
|
|
52
|
+
# rails g foreign_key 'users', 'roles'
|
|
53
|
+
#
|
|
54
|
+
# to build the migration containing
|
|
55
|
+
#
|
|
56
|
+
# def change
|
|
57
|
+
# add_foreign_key 'users', 'roles'
|
|
58
|
+
# end
|
|
59
|
+
#
|
|
60
|
+
def object_name
|
|
61
|
+
@object_name ||= operation.object.singularize
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Use the name of the object as a name of the generator class
|
|
65
|
+
def name
|
|
66
|
+
@name ||= "PGTrunk::Generators::#{object_name.camelize}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# The name of the operation to be added to the migration
|
|
70
|
+
def operation_name
|
|
71
|
+
@operation_name ||= operation.ruby_name
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Ruby handler to add positional arguments to options
|
|
75
|
+
def handle(*arguments, **options)
|
|
76
|
+
options.ruby_params.zip(arguments).merge(options)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def next_migration_number(dir)
|
|
80
|
+
::ActiveRecord::Generators::Base.next_migration_number(dir)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def create_migration_file
|
|
85
|
+
file_name = "db/migrate/#{migration_number}_#{migration_name}.rb"
|
|
86
|
+
file = create_migration(file_name, nil, {}) do
|
|
87
|
+
<<~RUBY
|
|
88
|
+
# frozen_string_literal: true
|
|
89
|
+
|
|
90
|
+
class #{migration_name.camelize} < #{migration_base}
|
|
91
|
+
def change
|
|
92
|
+
#{command}
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
RUBY
|
|
96
|
+
end
|
|
97
|
+
Rails::Generators.add_generated_file(file)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def current_version
|
|
103
|
+
return @current_version if instance_variable_defined?(:@current_version)
|
|
104
|
+
|
|
105
|
+
@current_version = nil
|
|
106
|
+
return unless ActiveRecord::Migration.respond_to?(:current_version)
|
|
107
|
+
|
|
108
|
+
@current_version = ActiveRecord::Migration.current_version
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Build the name of the migration from given params like:
|
|
112
|
+
#
|
|
113
|
+
# rails g foreign_key 'users', 'roles'
|
|
114
|
+
#
|
|
115
|
+
# to generate the migration named as:
|
|
116
|
+
#
|
|
117
|
+
# class AddForeignKeyUsersRoles < ::ActiveRecord::Migration[6.2]
|
|
118
|
+
# # ...
|
|
119
|
+
# end
|
|
120
|
+
def migration_name
|
|
121
|
+
@migration_name ||= [
|
|
122
|
+
self.class.operation_name,
|
|
123
|
+
*(self.class.operation.ruby_params.map { |p| send(p) }),
|
|
124
|
+
].join("_")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def migration_base
|
|
128
|
+
@migration_base ||= "::ActiveRecord::Migration".tap do |mb|
|
|
129
|
+
next if Rails::Version::MAJOR < "5"
|
|
130
|
+
|
|
131
|
+
mb << "[#{current_version}]" if current_version.present?
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def command
|
|
136
|
+
opts = self.class.handle(*arguments, **options.symbolize_keys)
|
|
137
|
+
operation = self.class.operation.new(opts)
|
|
138
|
+
operation.to_ruby.indent(4).strip
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class PGTrunk::Operation
|
|
4
|
+
# @private
|
|
5
|
+
# Define getters/setters for the operation attributes
|
|
6
|
+
module Attributes
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
include ActiveModel::Model
|
|
9
|
+
include ActiveModel::Attributes
|
|
10
|
+
|
|
11
|
+
# The special undefined value for getters/setters
|
|
12
|
+
# to distinct it from the explicitly provided `nil`.
|
|
13
|
+
UNDEFINED = Object.new.freeze
|
|
14
|
+
private_constant :UNDEFINED
|
|
15
|
+
|
|
16
|
+
class_methods do
|
|
17
|
+
# list of aliases for attributes
|
|
18
|
+
def attr_aliases
|
|
19
|
+
@attr_aliases ||= {}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Add an attribute to the operation
|
|
23
|
+
def attribute(name, type, default: nil, aliases: nil)
|
|
24
|
+
# prevent mutation of the default arrays, hashes etc.
|
|
25
|
+
default = default.freeze if default.respond_to?(:freeze)
|
|
26
|
+
super(name, type, default: default, &nil)
|
|
27
|
+
# add the private attribute for the previous value
|
|
28
|
+
attr_reader :"from_#{name}"
|
|
29
|
+
|
|
30
|
+
redefine_getter(name)
|
|
31
|
+
Array(aliases).each { |key| attr_aliases[key.to_sym] = name.to_sym }
|
|
32
|
+
name.to_sym
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def redefine_getter(name)
|
|
38
|
+
getter = instance_method(name)
|
|
39
|
+
define_method(name) do |value = UNDEFINED, *args, from: nil|
|
|
40
|
+
# fallback to the original getter w/o arguments
|
|
41
|
+
return getter.bind(self).call if value == UNDEFINED
|
|
42
|
+
|
|
43
|
+
# set a previous value to return to
|
|
44
|
+
instance_variable_set("@from_#{name}", from)
|
|
45
|
+
|
|
46
|
+
# arrays can be assigned as lists
|
|
47
|
+
value = [value, *args] if args.any?
|
|
48
|
+
|
|
49
|
+
send(:"#{name}=", value)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def inherited(klass)
|
|
54
|
+
klass.instance_variable_set(:@attr_aliases, attr_aliases)
|
|
55
|
+
super
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def initialize(**opts)
|
|
60
|
+
# enable aliases during the initialization
|
|
61
|
+
self.class.attr_aliases.each do |a, n|
|
|
62
|
+
opts[n] = opts.delete(a) if opts.key?(a)
|
|
63
|
+
end
|
|
64
|
+
# ignore unknown attributes (to simplify calls of `#invert`)
|
|
65
|
+
opts = opts.slice(*self.class.attribute_names.map(&:to_sym))
|
|
66
|
+
|
|
67
|
+
super(**opts)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# The hash of the operation's serialized attributes
|
|
71
|
+
def attributes
|
|
72
|
+
super.to_h do |k, v|
|
|
73
|
+
[k.to_sym, self.class.attribute_types[k].serialize(v)]
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
alias to_h attributes
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class PGTrunk::Operation
|
|
4
|
+
# @private
|
|
5
|
+
# Enable to fulfill/generate missed attributes
|
|
6
|
+
# using the `after_initialize` callback.
|
|
7
|
+
#
|
|
8
|
+
# The callback is invoked after the end of the normal
|
|
9
|
+
# initialization and applying a block with explicit settings.
|
|
10
|
+
module Callbacks
|
|
11
|
+
extend ActiveSupport::Concern
|
|
12
|
+
|
|
13
|
+
class_methods do
|
|
14
|
+
def callbacks
|
|
15
|
+
@callbacks ||= []
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Get or set the callback
|
|
19
|
+
def after_initialize(&block)
|
|
20
|
+
callbacks << block if block
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def inherited(klass)
|
|
26
|
+
klass.instance_variable_set(:@callbacks, callbacks.dup)
|
|
27
|
+
super
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private def initialize(*, **, &block)
|
|
32
|
+
# Explicitly assign all attributes from params/options.
|
|
33
|
+
super
|
|
34
|
+
# Explicitly assign attributes using a block.
|
|
35
|
+
block&.call(self)
|
|
36
|
+
# Apply +callback+ at the very end after all explicit assignments.
|
|
37
|
+
self.class.callbacks.each { |callback| instance_exec(&callback) }
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class PGTrunk::Operation
|
|
4
|
+
# @private
|
|
5
|
+
# Register attributes definition for later usage by generators
|
|
6
|
+
module Generators
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
class_methods do
|
|
10
|
+
# Gets or sets object name for the generator
|
|
11
|
+
def generates_object(name = nil)
|
|
12
|
+
@generates_object = name if name
|
|
13
|
+
@generates_object ||= nil
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# The definitions of the attributes
|
|
17
|
+
# @return [Hash{Symbol => Hash{type:, default:, desc:}}]
|
|
18
|
+
def attributes
|
|
19
|
+
@attributes ||= {}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def attribute(name, type, default: nil, desc: nil, **opts)
|
|
23
|
+
name = name.to_sym
|
|
24
|
+
attributes[name] = {
|
|
25
|
+
type: gen_type(type),
|
|
26
|
+
default: default,
|
|
27
|
+
desc: desc,
|
|
28
|
+
}
|
|
29
|
+
super(name, type.to_sym, default: default, **opts)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def inherited(klass)
|
|
35
|
+
klass.instance_variable_set(:@attributes, attributes.dup)
|
|
36
|
+
super
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Convert the type to the acceptable by Rails::Generator
|
|
40
|
+
def gen_type(type)
|
|
41
|
+
case type.to_s
|
|
42
|
+
when "bool", "boolean" then :boolean
|
|
43
|
+
when "integer", "float" then :numeric
|
|
44
|
+
when /^pg_trunk_array/ then :array
|
|
45
|
+
when /^pg_trunk_hash/ then :hash
|
|
46
|
+
else :string
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class PGTrunk::Operation
|
|
4
|
+
# @private
|
|
5
|
+
# The exception to be thrown when reversed migration isn't valid
|
|
6
|
+
class IrreversibleMigration < ActiveRecord::IrreversibleMigration
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def initialize(operation, inversion, *messages)
|
|
10
|
+
msg = "#{header(operation)}#{inverted(inversion)} #{footer(messages)}"
|
|
11
|
+
super(msg.strip)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def header(operation)
|
|
15
|
+
<<~MSG
|
|
16
|
+
This migration uses the operation:
|
|
17
|
+
|
|
18
|
+
#{operation.to_ruby.indent(2).strip}
|
|
19
|
+
|
|
20
|
+
MSG
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def inverted(inversion)
|
|
24
|
+
return "which is not automatically reversible" unless inversion
|
|
25
|
+
|
|
26
|
+
<<~MSG.strip
|
|
27
|
+
whose inversion would be like:
|
|
28
|
+
|
|
29
|
+
#{inversion.to_ruby.indent(2).strip}
|
|
30
|
+
|
|
31
|
+
which is invalid
|
|
32
|
+
MSG
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def footer(messages)
|
|
36
|
+
reasons = <<~REASONS.strip if messages.any?
|
|
37
|
+
for the following reasons:
|
|
38
|
+
|
|
39
|
+
#{messages.map { |m| "- #{m}" }.join("\n")}
|
|
40
|
+
REASONS
|
|
41
|
+
|
|
42
|
+
<<~MSG.strip
|
|
43
|
+
#{reasons}
|
|
44
|
+
|
|
45
|
+
To make the migration reversible you can either:
|
|
46
|
+
1. Define #up and #down methods in place of the #change method.
|
|
47
|
+
2. Use the #reversible method to define reversible behavior.
|
|
48
|
+
MSG
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @private
|
|
53
|
+
# Enable operations to be invertible
|
|
54
|
+
module Inversion
|
|
55
|
+
# @private
|
|
56
|
+
def invert!
|
|
57
|
+
invert&.tap do |i|
|
|
58
|
+
i.valid? || raise(IrreversibleMigration.new(self, i, *i.error_messages))
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @private
|
|
63
|
+
def irreversible!(option)
|
|
64
|
+
raise IrreversibleMigration.new(self, nil, <<~MSG.squish)
|
|
65
|
+
The operation with the `#{option}` option cannot be reversed
|
|
66
|
+
due to uncertainty of the previous state of the database.
|
|
67
|
+
MSG
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class PGTrunk::Operation
|
|
4
|
+
# @private
|
|
5
|
+
# Invoke all the necessary definitions
|
|
6
|
+
# in the modules included to Rails via Railtie
|
|
7
|
+
module Registration
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
class_methods do
|
|
11
|
+
def from_sql(&block)
|
|
12
|
+
super.tap { register_dumper if block }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def generates_object(name = nil)
|
|
16
|
+
super.tap { register_generator if name }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def method_added(name)
|
|
20
|
+
super
|
|
21
|
+
ensure
|
|
22
|
+
register_operation if name == :to_sql
|
|
23
|
+
register_inversion if name == :invert
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def register_operation
|
|
29
|
+
# Add the method to statements as an entry point
|
|
30
|
+
PGTrunk::Statements.register(self)
|
|
31
|
+
# Add the shortcut to migration go get away with checking
|
|
32
|
+
# of the first parameter which could be NOT a table name.
|
|
33
|
+
PGTrunk::Migration.register(self)
|
|
34
|
+
# Record the direct operation
|
|
35
|
+
PGTrunk::CommandRecorder.register(self)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def register_inversion
|
|
39
|
+
# Record the inversion of the operation
|
|
40
|
+
PGTrunk::CommandRecorder.register_inversion(self)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def register_dumper
|
|
44
|
+
PGTrunk::SchemaDumper.register(self)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def register_generator
|
|
48
|
+
# skip registration in the runtime
|
|
49
|
+
return unless const_defined?("PGTrunk::Generators")
|
|
50
|
+
|
|
51
|
+
PGTrunk::Generators.register(self)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class PGTrunk::Operation
|
|
4
|
+
# @private
|
|
5
|
+
# Build ruby snippet
|
|
6
|
+
class RubyBuilder
|
|
7
|
+
private def initialize(name, shortage: nil)
|
|
8
|
+
@args = []
|
|
9
|
+
@lines = []
|
|
10
|
+
@name = name&.to_s
|
|
11
|
+
@opts = []
|
|
12
|
+
@shortage = shortage
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Add parameters to the method call
|
|
16
|
+
def ruby_param(*args, **opts)
|
|
17
|
+
@args = [*@args, *params(*args)]
|
|
18
|
+
@opts = [*@opts, *params(**opts)]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Add line into a block
|
|
22
|
+
def ruby_line(meth, *args, **opts)
|
|
23
|
+
return if meth.blank?
|
|
24
|
+
return if args.first.nil?
|
|
25
|
+
|
|
26
|
+
@lines << build_line(meth, *args, **opts)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Build the snippet
|
|
30
|
+
# @return [String]
|
|
31
|
+
def build
|
|
32
|
+
[header, *block].join(" ")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# Pattern to split lines by heredocs
|
|
38
|
+
HEREDOC = /<<~'?(?<head>[A-Z]+)'?.+(?<body>\n .+)*\n\k<head>/.freeze
|
|
39
|
+
|
|
40
|
+
def build_line(meth, *args, **opts)
|
|
41
|
+
method_name = [shortage, meth].join(".")
|
|
42
|
+
method_params = params(*args, **opts)
|
|
43
|
+
line = [method_name, *method_params].join(" ")
|
|
44
|
+
return single_line(line).indent(2) unless block_given?
|
|
45
|
+
|
|
46
|
+
builder = self.class.new(line, shortage: "f")
|
|
47
|
+
yield(builder)
|
|
48
|
+
builder.build.indent(2)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Finalize line containing a heredoc args
|
|
52
|
+
# "body <<~'SQL'.chomp\n foo\nSQL, from: <<~'SQL'.chomp\n bar\nSQL"
|
|
53
|
+
# "body <<~'SQL'.chomp, from: <<~'SQL'.chomp\n foo\nSQL\n bar\nSQL"
|
|
54
|
+
def single_line(text)
|
|
55
|
+
parts = text.partition(HEREDOC)
|
|
56
|
+
(
|
|
57
|
+
parts.map { |p| p[/^.+/] } + parts.map { |p| p[/\n(\n|.)*$/] }
|
|
58
|
+
).compact.join
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def shortage
|
|
62
|
+
@shortage ||= @name.split("_").last.first
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def format(value)
|
|
66
|
+
case value
|
|
67
|
+
when Hash then value
|
|
68
|
+
when String then format_text(value)
|
|
69
|
+
when Array then format_list(value)
|
|
70
|
+
else value.inspect
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def format_text(text)
|
|
75
|
+
text = text.chomp
|
|
76
|
+
# prevent quoting interpolations and heredocs
|
|
77
|
+
return text if text[/^<<~|^%[A-Za-z][(]/]
|
|
78
|
+
|
|
79
|
+
long_text = text.size > 50 || text.include?("\n")
|
|
80
|
+
return "<<~'Q'.chomp\n#{text.indent(2)}\nQ" if long_text && text["\\"]
|
|
81
|
+
return "<<~Q.chomp\n#{text.indent(2)}\nQ" if long_text
|
|
82
|
+
return "%q(#{text})" if /\\|"/.match?(text)
|
|
83
|
+
|
|
84
|
+
text.inspect
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def format_list(list)
|
|
88
|
+
case list.map(&:class).uniq
|
|
89
|
+
when [::String] then "%w[#{list.join(' ')}]"
|
|
90
|
+
when [::Symbol] then "%i[#{list.join(' ')}]"
|
|
91
|
+
else list
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def params(*values, **options)
|
|
96
|
+
vals = values.map { |val| format(val) }
|
|
97
|
+
opts = options.compact.map { |key, val| "#{key}: #{format(val)}" }
|
|
98
|
+
[*vals, *opts].join(", ").presence
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def header
|
|
102
|
+
method_params = [*@args, *@opts].join(", ").presence
|
|
103
|
+
line = [@name, *method_params].join(" ")
|
|
104
|
+
line << " do |#{shortage}|" if @lines.any?
|
|
105
|
+
single_line(line)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def block
|
|
109
|
+
[nil, *@lines, "end"].join("\n") if @lines.any?
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|