pg_trunk 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|