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,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class PGTrunk::Operation
|
|
4
|
+
# @private
|
|
5
|
+
# Helpers to build ruby snippet from the operation definition
|
|
6
|
+
module RubyHelpers
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
class_methods do
|
|
10
|
+
# The name of the builder
|
|
11
|
+
# @return [Symbol]
|
|
12
|
+
def ruby_name
|
|
13
|
+
@ruby_name ||= name.split("::").last.underscore.to_sym
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# The name of the inverted builder
|
|
17
|
+
# @return [Symbol]
|
|
18
|
+
def ruby_iname
|
|
19
|
+
"invert_#{ruby_name}".to_sym
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Get/set positional params of the ruby method
|
|
23
|
+
#
|
|
24
|
+
# @example Provide a method `add_check_constraint(table = nil, **opts)`
|
|
25
|
+
# class AddCheckConstraint < PGTrunk::Operation
|
|
26
|
+
# ruby_params :table
|
|
27
|
+
# # ...
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
def ruby_params(*params)
|
|
31
|
+
@ruby_params = params.compact.map(&:to_sym) if params.any?
|
|
32
|
+
@ruby_params ||= []
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Gets or sets the block building a ruby snippet
|
|
36
|
+
#
|
|
37
|
+
# @yieldparam [PGTrunk::Operation::RubyBuilder]
|
|
38
|
+
#
|
|
39
|
+
# @example
|
|
40
|
+
# ruby_snippet do |s|
|
|
41
|
+
# s.ruby_param(comment: comment) if comment.present?
|
|
42
|
+
# values.each { |v| s.ruby_line(:value, v) }
|
|
43
|
+
# end
|
|
44
|
+
#
|
|
45
|
+
# # will build something like
|
|
46
|
+
#
|
|
47
|
+
# do_something "foo.bar", comment: "comment" do |s|
|
|
48
|
+
# s.value "baz"
|
|
49
|
+
# s.value "qux"
|
|
50
|
+
# end
|
|
51
|
+
def ruby_snippet(&block)
|
|
52
|
+
@ruby_snippet ||= block
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Build the operation from arguments sent to Ruby method
|
|
56
|
+
def from_ruby(*args, &block)
|
|
57
|
+
options = args.last.is_a?(Hash) ? args.pop.symbolize_keys : {}
|
|
58
|
+
params = ruby_params.zip(args).to_h
|
|
59
|
+
new(**params, **options, &block)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def inherited(klass)
|
|
65
|
+
# Use params from a parent class by default (can be overloaded).
|
|
66
|
+
klass.instance_variable_set(:@ruby_params, ruby_params)
|
|
67
|
+
klass.instance_variable_set(:@ruby_snippet, ruby_snippet)
|
|
68
|
+
super
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# @private
|
|
73
|
+
# Ruby snippet to dump the creator
|
|
74
|
+
# @return [String]
|
|
75
|
+
def to_ruby
|
|
76
|
+
builder = RubyBuilder.new(self.class.ruby_name)
|
|
77
|
+
instance_exec(builder, &self.class.ruby_snippet)
|
|
78
|
+
builder.build.rstrip
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# List of attributes assigned that are assigned
|
|
82
|
+
# via Ruby method parameters.
|
|
83
|
+
#
|
|
84
|
+
# We can use it to announce the operation to $stdout
|
|
85
|
+
# like `create_foreign_key("users", "roles")`.
|
|
86
|
+
def to_a
|
|
87
|
+
to_h.values_at(*self.class.ruby_params)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def to_opts
|
|
91
|
+
to_h.except(*self.class.ruby_params)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# @param [IO] stream
|
|
95
|
+
def dump(stream)
|
|
96
|
+
to_ruby&.rstrip&.lines&.each { |line| stream.print(line.indent(2)) }
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class PGTrunk::Operation
|
|
4
|
+
# @private
|
|
5
|
+
# Add helpers for building SQL queries
|
|
6
|
+
module SQLHelpers
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
class_methods do
|
|
10
|
+
include Enumerable
|
|
11
|
+
|
|
12
|
+
# Get/set the block to extract operation definitions
|
|
13
|
+
# from the database.
|
|
14
|
+
# @yield [Proc] the block returning sql
|
|
15
|
+
# @yieldparam [#to_s] version The current version of the database
|
|
16
|
+
def from_sql(&block)
|
|
17
|
+
@from_sql = block if block
|
|
18
|
+
@from_sql ||= nil
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Iterate by sorted operation definitions
|
|
22
|
+
# extracted from the database
|
|
23
|
+
def each(&block)
|
|
24
|
+
return to_enum unless block_given?
|
|
25
|
+
|
|
26
|
+
fetch
|
|
27
|
+
.map { |item| new(**item.symbolize_keys) }
|
|
28
|
+
.sort
|
|
29
|
+
.each { |op| block.call(op) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def fetch
|
|
35
|
+
query = from_sql&.call(PGTrunk.database.server_version)
|
|
36
|
+
query.blank? ? [] : PGTrunk.database.execute(query)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def quote(str)
|
|
41
|
+
PGTrunk.database.quote(str)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class PGTrunk::Operation
|
|
4
|
+
# @private
|
|
5
|
+
# Enable validation of the operation in the Rails way
|
|
6
|
+
module Validations
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
class_methods do
|
|
10
|
+
extend ActiveModel::Validations
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def error_messages
|
|
14
|
+
errors.messages.flat_map do |k, v|
|
|
15
|
+
Array(v).map do |msg|
|
|
16
|
+
k == :base ? msg : [k, msg].join(" ")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: false
|
|
2
|
+
|
|
3
|
+
require_relative "operation/callbacks"
|
|
4
|
+
require_relative "operation/attributes"
|
|
5
|
+
require_relative "operation/generators"
|
|
6
|
+
require_relative "operation/validations"
|
|
7
|
+
require_relative "operation/inversion"
|
|
8
|
+
require_relative "operation/ruby_builder"
|
|
9
|
+
require_relative "operation/ruby_helpers"
|
|
10
|
+
require_relative "operation/sql_helpers"
|
|
11
|
+
require_relative "operation/registration"
|
|
12
|
+
|
|
13
|
+
module PGTrunk
|
|
14
|
+
# @private
|
|
15
|
+
# Base class for operations.
|
|
16
|
+
# Inherit this class to define new operation.
|
|
17
|
+
class Operation
|
|
18
|
+
include Callbacks
|
|
19
|
+
include Attributes
|
|
20
|
+
include Generators
|
|
21
|
+
include Comparable
|
|
22
|
+
include Validations
|
|
23
|
+
include Inversion
|
|
24
|
+
include RubyHelpers
|
|
25
|
+
include SQLHelpers
|
|
26
|
+
include Registration
|
|
27
|
+
|
|
28
|
+
attribute :comment, :string, desc: \
|
|
29
|
+
"The comment to the object"
|
|
30
|
+
attribute :force, :pg_trunk_symbol, desc: \
|
|
31
|
+
"How to process dependent objects"
|
|
32
|
+
attribute :if_exists, :boolean, desc: \
|
|
33
|
+
"Don't fail if the object is absent"
|
|
34
|
+
attribute :if_not_exists, :boolean, desc: \
|
|
35
|
+
"Don't fail if the object is already present"
|
|
36
|
+
attribute :name, :pg_trunk_qualified_name, desc: \
|
|
37
|
+
"The qualified name of the object"
|
|
38
|
+
attribute :new_name, :pg_trunk_qualified_name, aliases: :to, desc: \
|
|
39
|
+
"The new name of the object to rename to"
|
|
40
|
+
attribute :oid, :integer, desc: \
|
|
41
|
+
"The oid of the database object"
|
|
42
|
+
attribute :version, :integer, aliases: :revert_to_version, desc: \
|
|
43
|
+
"The version of the SQL snippet"
|
|
44
|
+
|
|
45
|
+
validates :name, presence: true
|
|
46
|
+
validates :new_name, "PGTrunk/difference": { from: :name }, allow_nil: true
|
|
47
|
+
validates :force, inclusion: { in: %i[cascade restrict] }, allow_nil: true
|
|
48
|
+
|
|
49
|
+
# By default ruby methods take the object name as a positional argument.
|
|
50
|
+
ruby_params :name
|
|
51
|
+
|
|
52
|
+
protected
|
|
53
|
+
|
|
54
|
+
# Define the order of objects
|
|
55
|
+
# @param [PGTrunk::Definitions]
|
|
56
|
+
# @return [-1, 0, 1, nil]
|
|
57
|
+
def <=>(other)
|
|
58
|
+
name <=> other.name if other.is_a?(self.class)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
# Helper to read a versioned snippet for a specific
|
|
64
|
+
# kind of objects
|
|
65
|
+
def read_snippet_from(kind)
|
|
66
|
+
return if kind.blank? || name.blank? || version.blank?
|
|
67
|
+
|
|
68
|
+
filename = format(
|
|
69
|
+
"db/%<kind>s/%<name>s_v%<version>02d.sql",
|
|
70
|
+
kind: kind.to_s.pluralize,
|
|
71
|
+
name: name.routine,
|
|
72
|
+
version: version,
|
|
73
|
+
)
|
|
74
|
+
filepath = Rails.root.join(filename)
|
|
75
|
+
File.read(filepath).sub(/;\s*$/, "")
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PGTrunk
|
|
4
|
+
# @private
|
|
5
|
+
# The qualified name of an object consists from schema (namespace) and name.
|
|
6
|
+
# The class contains several helper methods for ruby and sql snippets.
|
|
7
|
+
QualifiedName = Struct.new(:schema, :name) do
|
|
8
|
+
include Comparable
|
|
9
|
+
|
|
10
|
+
# Build qualified name structure from a string
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# QualifiedName["bar"].to_h
|
|
14
|
+
# # => { schema: "public", name: "bar" }
|
|
15
|
+
#
|
|
16
|
+
# QualifiedName["foo.bar(a int, b int)"].to_h
|
|
17
|
+
# # => { schema: "foo", name: "bar(a int, b int)" }
|
|
18
|
+
#
|
|
19
|
+
def self.wrap(string)
|
|
20
|
+
schema = /^([^.(]+)[.]/.match(string).to_a[1]
|
|
21
|
+
name = string.sub(/^[^.(]*[.]/, "")
|
|
22
|
+
new(schema, name)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Get the current schema
|
|
26
|
+
def current_schema
|
|
27
|
+
@current_schema ||= PGTrunk.database.current_schema
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private def initialize(schema, name)
|
|
31
|
+
super(schema.to_s.presence || current_schema, name.to_s.presence)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# If the schema is current (which is usually 'public')
|
|
35
|
+
# This schema would be ignored in +lean+
|
|
36
|
+
def current_schema?
|
|
37
|
+
schema == current_schema
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# If the qualified name is blank (not specified)
|
|
41
|
+
def blank?
|
|
42
|
+
routine.blank?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# If the name matches the regex
|
|
46
|
+
def match?(regex)
|
|
47
|
+
routine.match?(regex)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# If the name is present but not matches the regexp
|
|
51
|
+
# (used to check if it is not generated by some pattern)
|
|
52
|
+
def differs_from?(regex)
|
|
53
|
+
!routine.match?(regex) if present?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Quoted representation of the name for SQL snippets
|
|
57
|
+
#
|
|
58
|
+
# QualifiedName.wrap("foo.bar(a int, OUT b int) record").to_sql
|
|
59
|
+
# # => '"foo"."bar" (a int)'
|
|
60
|
+
#
|
|
61
|
+
# qname = QualifiedName.wrap("public.foo")
|
|
62
|
+
# qname.to_sql
|
|
63
|
+
# # => '"foo"'
|
|
64
|
+
# qname.to_sql(true)
|
|
65
|
+
# # => '"foo"()'
|
|
66
|
+
def to_sql(with_args = nil)
|
|
67
|
+
@to_sql ||= begin
|
|
68
|
+
str = [
|
|
69
|
+
*(schema unless current_schema?),
|
|
70
|
+
routine,
|
|
71
|
+
].map(&:inspect).join(".")
|
|
72
|
+
str = "#{str} (#{args})" if args.present? || with_args
|
|
73
|
+
str
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# The qualified name with unquoted parts
|
|
78
|
+
# (to be used in ruby snippets)
|
|
79
|
+
#
|
|
80
|
+
# QualifiedName.wrap("bar.foo").full
|
|
81
|
+
# # => "bar.foo"
|
|
82
|
+
#
|
|
83
|
+
# QualifiedName.wrap("public.foo(int, int) int").full
|
|
84
|
+
# # => "foo(int, int) int"
|
|
85
|
+
def full
|
|
86
|
+
@full ||= [schema, name].join(".")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Exclude current schema from a qualified name
|
|
90
|
+
#
|
|
91
|
+
# QualifiedName.wrap("public.foo").lean
|
|
92
|
+
# # => "foo"
|
|
93
|
+
#
|
|
94
|
+
# QualifiedName.wrap("bar.baz").lean
|
|
95
|
+
# # => "bar.baz"
|
|
96
|
+
def lean
|
|
97
|
+
@lean ||= [*(schema unless current_schema?), name].join(".")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Name of the routine for function definition
|
|
101
|
+
# QualifiedName.wrap("foo.bar(a int, b int) int").routine
|
|
102
|
+
# # => "bar"
|
|
103
|
+
def routine
|
|
104
|
+
@routine ||= name[/^[^(]+/]&.strip
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Args from the function definition
|
|
108
|
+
# QualifiedName.wrap("foo.bar(a int, b int DEFAULT = 0) int").args
|
|
109
|
+
# # => "a int, b int DEFAULT = 0"
|
|
110
|
+
def args
|
|
111
|
+
@args ||= name&.match(/\(([^)]+)/).to_a.last&.gsub(/\s+/, " ")&.strip
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Arg types from the function definition
|
|
115
|
+
# QualifiedName.wrap("foo.bar(a int, b int DEFAULT = 0) int").args
|
|
116
|
+
# # => %w[int int]
|
|
117
|
+
def arg_types
|
|
118
|
+
@arg_types ||= args.to_s.split(",").map { |a| a.strip.split(/\s+/).last }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Type of returned value of a routine
|
|
122
|
+
# QualifiedName.wrap("foo.concat(a int, b int) text").returns
|
|
123
|
+
# # => "text"
|
|
124
|
+
def returns
|
|
125
|
+
@returns ||= name&.match(/\)(.+)/).to_a.last&.strip
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Quote the name (to compare to text fields in the database)
|
|
129
|
+
# QualifiedName.wrap("foo.concat(a int, b int) text").quoted
|
|
130
|
+
# # => "'concat'"
|
|
131
|
+
def quoted
|
|
132
|
+
@quoted ||= PGTrunk.database.quote(routine)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Transform the namespace (to compare to oid fields in the database)
|
|
136
|
+
# QualifiedName.wrap("foo.bar").namespace
|
|
137
|
+
# # => "'foo'::regnamespace"
|
|
138
|
+
def namespace
|
|
139
|
+
@namespace ||= "#{PGTrunk.database.quote(schema)}::regnamespace"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Make qualified names comparable by their full representation
|
|
143
|
+
# @return [Integer, NilClass]
|
|
144
|
+
def <=>(other)
|
|
145
|
+
sort_order <=> other.sort_order if other.is_a?(self.class)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Checks if partially qualified <function>
|
|
149
|
+
# have the same schema/name as another function
|
|
150
|
+
def maybe_eq?(other)
|
|
151
|
+
sort_order.first(2) == other.sort_order.first(2)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Build the name with a changed part (either a name or a schema)
|
|
155
|
+
def merge(**args)
|
|
156
|
+
self.class.new(args.fetch(:schema, schema), args.fetch(:name, name))
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
protected
|
|
160
|
+
|
|
161
|
+
def sort_order
|
|
162
|
+
[schema.to_s, routine.to_s, *arg_types]
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PGTrunk
|
|
4
|
+
# @private
|
|
5
|
+
# The module record commands done during a migration.
|
|
6
|
+
module CommandRecorder
|
|
7
|
+
# @param [PGTrunk::Operation] klass
|
|
8
|
+
def self.register(klass)
|
|
9
|
+
define_method(klass.ruby_name) do |*args, &block|
|
|
10
|
+
record(klass.ruby_name, args, &block)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @param [PGTrunk::Operation] klass
|
|
15
|
+
def self.register_inversion(klass)
|
|
16
|
+
define_method(klass.ruby_iname) do |args, &block|
|
|
17
|
+
original = klass.from_ruby(*args, &block)
|
|
18
|
+
inverted = original.invert!
|
|
19
|
+
# for example (skip_inversion(:validate_foreign_key))
|
|
20
|
+
return [:skip_inversion, [klass.ruby_name]] unless inverted
|
|
21
|
+
|
|
22
|
+
# list of attributes `to_a` is added for reporting to stdout
|
|
23
|
+
params = inverted.to_a
|
|
24
|
+
opts = inverted.to_opts
|
|
25
|
+
params << opts if opts.present?
|
|
26
|
+
[inverted.class.ruby_name, params]
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PGTrunk
|
|
4
|
+
# @private
|
|
5
|
+
# The module adds custom type casting
|
|
6
|
+
module CustomTypes
|
|
7
|
+
# All custom types are typecasted to strings in Rails
|
|
8
|
+
TYPE = ActiveRecord::ConnectionAdapters::PostgreSQL::OID::SpecializedString
|
|
9
|
+
|
|
10
|
+
def self.known
|
|
11
|
+
@known ||= Set.new([])
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def enable_pg_trunk_types
|
|
15
|
+
execute(<<~SQL).each { |item| enable_pg_trunk_type item["name"] }
|
|
16
|
+
SELECT (
|
|
17
|
+
CASE
|
|
18
|
+
WHEN t.typnamespace = 'public'::regnamespace THEN t.typname
|
|
19
|
+
ELSE t.typnamespace::regnamespace || '.' || t.typname
|
|
20
|
+
END
|
|
21
|
+
) AS name
|
|
22
|
+
FROM pg_trunk e JOIN pg_type t ON t.oid = e.oid
|
|
23
|
+
WHERE e.classid = 'pg_type'::regclass
|
|
24
|
+
SQL
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def enable_pg_trunk_type(type)
|
|
28
|
+
type = type.to_s
|
|
29
|
+
CustomTypes.known << type
|
|
30
|
+
type_map.register_type(type, TYPE.new(type)) unless type_map.key?(type)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def valid_type?(type)
|
|
34
|
+
CustomTypes.known.include?(type.to_s) || super
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PGTrunk
|
|
4
|
+
# @private
|
|
5
|
+
# The module goes around the ActiveRecord::Migration's `method_missing`.
|
|
6
|
+
#
|
|
7
|
+
# This is necessary because +ActiveRecord::Migration#method_missing+
|
|
8
|
+
# forces the first argument to be a proper table name.
|
|
9
|
+
#
|
|
10
|
+
# In Rails migrations the first argument specifies the +table+,
|
|
11
|
+
# while the +name+ of the object can be specified by option:
|
|
12
|
+
#
|
|
13
|
+
# pg_trunk_create_index :users, %w[id name], name: 'users_idx'
|
|
14
|
+
#
|
|
15
|
+
# in PGTrunk the positional argument is always specify the name
|
|
16
|
+
# of the the current object (type, function, table etc.):
|
|
17
|
+
#
|
|
18
|
+
# pg_trunk_create_index 'users_ids', table: 'users', columns: %w[id name]
|
|
19
|
+
# create_enum 'currency', values: %w[USD EUR BTC]
|
|
20
|
+
#
|
|
21
|
+
# With this fix we can also use the options-only syntax like:
|
|
22
|
+
#
|
|
23
|
+
# pg_trunk_create_enum name: 'currency', values: %w[USD EUR BTC]
|
|
24
|
+
#
|
|
25
|
+
# or even skip any name when it can be generated from options:
|
|
26
|
+
#
|
|
27
|
+
# pg_trunk_create_index do |i|
|
|
28
|
+
# i.table 'users'
|
|
29
|
+
# i.column 'id'
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
module Migration
|
|
33
|
+
# @param [PGTrunk::Operation] klass
|
|
34
|
+
def self.register(klass)
|
|
35
|
+
define_method(klass.ruby_name) do |*args, &block|
|
|
36
|
+
say_with_time "#{klass.ruby_name}(#{_pretty_args(*args)})" do
|
|
37
|
+
connection.send(klass.ruby_name, *args, &block)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def _pretty_args(*args)
|
|
45
|
+
opts = args.last.is_a?(Hash) ? args.pop : {}
|
|
46
|
+
opts = opts.map { |k, v| "#{k}: #{v.inspect}" if v.present? }.compact
|
|
47
|
+
[*args.map(&:inspect), *opts].join(", ")
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PGTrunk
|
|
4
|
+
# @private
|
|
5
|
+
# This module extends the ActiveRecord::Migrator
|
|
6
|
+
# to clean the gem-specific registry `pg_trunk`:
|
|
7
|
+
#
|
|
8
|
+
# - set version to rows added by the current migration
|
|
9
|
+
# - delete rows that refer to objects deleted by the migration
|
|
10
|
+
#
|
|
11
|
+
# We need this because some objects (like check constraints,
|
|
12
|
+
# indexes etc.) can be dropped along with the table
|
|
13
|
+
# they refer to. This depencency is implicit-ish.
|
|
14
|
+
# That's why we have to check the presence of all objects in `pg_trunk`
|
|
15
|
+
# after every single migration.
|
|
16
|
+
module Migrator
|
|
17
|
+
def record_version_state_after_migrating(*)
|
|
18
|
+
super
|
|
19
|
+
PGTrunk::Registry.finalize
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PGTrunk
|
|
4
|
+
# @private
|
|
5
|
+
# Overloads methods defined in ActiveRecord::SchemaDumper
|
|
6
|
+
# to redefine how various objects must be dumped.
|
|
7
|
+
module SchemaDumper
|
|
8
|
+
class << self
|
|
9
|
+
def operations
|
|
10
|
+
@operations ||= []
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def register(operation)
|
|
14
|
+
operations << operation unless operations.include?(operation)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Here we totally redefining the way a schema is dumped.
|
|
19
|
+
#
|
|
20
|
+
# In Rails every table definition is dumped as an `add_table`
|
|
21
|
+
# creator including all its columns, indexes, type casts and foreign keys.
|
|
22
|
+
#
|
|
23
|
+
# In some circumstances, these objects can have inter-dependencies
|
|
24
|
+
# with others (like functions, custom types and constraints).
|
|
25
|
+
# For example, we could define a function getting table raw as an argument,
|
|
26
|
+
# and then use this function to define check constraint for the table.
|
|
27
|
+
# In this case we must insert the definition of the function between
|
|
28
|
+
# the table's and constraint's ones.
|
|
29
|
+
#
|
|
30
|
+
# That's why we can neither rely on the method, defined in ActiveRecord
|
|
31
|
+
# nor reuse it through fallback to `super` like both Scenic and F(x) do.
|
|
32
|
+
# Instead of it, we fetch object definitions from the database,
|
|
33
|
+
# and then resolve their inter-dependencies.
|
|
34
|
+
def dump(stream)
|
|
35
|
+
pg_trunk_register_custom_types
|
|
36
|
+
header(stream)
|
|
37
|
+
extensions(stream)
|
|
38
|
+
pg_trunk_objects(stream)
|
|
39
|
+
trailer(stream)
|
|
40
|
+
stream
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
# Before dumping the schema extract from SQL all custom types
|
|
46
|
+
# to enable their usage in table columns in the schema.
|
|
47
|
+
def pg_trunk_register_custom_types
|
|
48
|
+
@connection.enable_pg_trunk_types
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def pg_trunk_objects(stream)
|
|
52
|
+
# Fetch operation definitions from the database.
|
|
53
|
+
#
|
|
54
|
+
# Operations of different kind are fetched
|
|
55
|
+
# in the order of their definitions (see `lib/pg_trunk/definitions.rb`).
|
|
56
|
+
# Operations of the same kind are sorted in a kind-specific order.
|
|
57
|
+
operations = SchemaDumper.operations.flat_map(&:to_a)
|
|
58
|
+
# Limit operations by oids known in `pg_trunk`
|
|
59
|
+
oids = PGTrunk::Registry.pluck(:oid)
|
|
60
|
+
operations = operations.select { |op| oids.include?(op.oid) }
|
|
61
|
+
# Resolve dependencies between fetched commands.
|
|
62
|
+
operations = PGTrunk::DependenciesResolver.resolve(operations)
|
|
63
|
+
# provide the content of the schema.
|
|
64
|
+
operations.each do |cmd|
|
|
65
|
+
cmd.dump(stream)
|
|
66
|
+
stream.puts
|
|
67
|
+
stream.puts
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Prevent indexes and check constraints from being added to the table
|
|
72
|
+
def indexes_in_create(_table, _stream); end
|
|
73
|
+
def check_constraints_in_create(_table, _stream); end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PGTrunk
|
|
4
|
+
# @private
|
|
5
|
+
# The module makes `pg_trunk` gem-specific registry
|
|
6
|
+
# to be created and dropped along with the native schema.
|
|
7
|
+
module SchemaMigration
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
class_methods do
|
|
11
|
+
def create_table
|
|
12
|
+
super
|
|
13
|
+
PGTrunk::Registry.create_table
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def drop_table
|
|
17
|
+
PGTrunk::Registry.drop_table
|
|
18
|
+
super
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PGTrunk
|
|
4
|
+
# @private
|
|
5
|
+
# The module adds commands to execute DDL operations in PostgreSQL.
|
|
6
|
+
module Statements
|
|
7
|
+
# @param [PGTrunk::Operation] klass
|
|
8
|
+
def self.register(klass)
|
|
9
|
+
define_method(klass.ruby_name) do |*args, &block|
|
|
10
|
+
operation = klass.from_ruby(*args, &block)
|
|
11
|
+
operation.validate!
|
|
12
|
+
PGTrunk.database.execute_operation(operation)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# A command does nothing when a unidirectional command is inverted
|
|
17
|
+
# (for example, when a foreign key validation is inverted).
|
|
18
|
+
# This case is different from those when an inversion cannot be made.
|
|
19
|
+
def skip_inversion(*); end
|
|
20
|
+
end
|
|
21
|
+
end
|