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,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
|