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,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# nodoc
|
|
4
|
+
module PGTrunk::Operations
|
|
5
|
+
# @private
|
|
6
|
+
# Definitions for composite types
|
|
7
|
+
module CompositeTypes
|
|
8
|
+
require_relative "composite_types/column"
|
|
9
|
+
require_relative "composite_types/base"
|
|
10
|
+
require_relative "composite_types/change_composite_type"
|
|
11
|
+
require_relative "composite_types/create_composite_type"
|
|
12
|
+
require_relative "composite_types/drop_composite_type"
|
|
13
|
+
require_relative "composite_types/rename_composite_type"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: false
|
|
2
|
+
|
|
3
|
+
module PGTrunk::Operations::Domains
|
|
4
|
+
# @abstract
|
|
5
|
+
# @private
|
|
6
|
+
# Base class for operations with domain types
|
|
7
|
+
class Base < PGTrunk::Operation
|
|
8
|
+
# All attributes that can be used by domain-related commands
|
|
9
|
+
attribute :collation, :pg_trunk_qualified_name
|
|
10
|
+
attribute :constraints, :pg_trunk_array_of_hashes, default: []
|
|
11
|
+
attribute :default_sql, :pg_trunk_multiline_text
|
|
12
|
+
attribute :null, :boolean
|
|
13
|
+
attribute :type, :pg_trunk_qualified_name, aliases: :as
|
|
14
|
+
|
|
15
|
+
# Populate constraints from a block
|
|
16
|
+
def constraint(check, name: nil)
|
|
17
|
+
constraints << Constraint.new(check: check, name: name)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Wrap constraint definitions to value objects
|
|
21
|
+
after_initialize { constraints.map! { |c| Constraint.build(c) } }
|
|
22
|
+
|
|
23
|
+
validates :if_not_exists, absence: true
|
|
24
|
+
validates :name, presence: true
|
|
25
|
+
validates :constraints, "PGTrunk/all_items_valid": true, allow_nil: true
|
|
26
|
+
|
|
27
|
+
# Use comparison by name from pg_trunk operations base class (default)
|
|
28
|
+
# Support name as the only positional argument (default)
|
|
29
|
+
|
|
30
|
+
ruby_snippet do |s|
|
|
31
|
+
s.ruby_param(name.lean) if name.present?
|
|
32
|
+
s.ruby_param(as: type.lean) if type.present?
|
|
33
|
+
s.ruby_param(to: new_name) if new_name.present?
|
|
34
|
+
s.ruby_param(if_exists: true) if if_exists
|
|
35
|
+
s.ruby_param(force: :cascade) if force == :cascade
|
|
36
|
+
|
|
37
|
+
s.ruby_line(:collation, collation.lean) if collation.present?
|
|
38
|
+
s.ruby_line(:default_sql, default_sql, from: from_default_sql) if default_sql
|
|
39
|
+
s.ruby_line(:null, false) if null == false
|
|
40
|
+
constraints.sort_by(&:name).each do |c|
|
|
41
|
+
s.ruby_line(:constraint, c.check, **c.opts)
|
|
42
|
+
end
|
|
43
|
+
s.ruby_line(:comment, comment, from: from_comment) if comment
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: false
|
|
2
|
+
|
|
3
|
+
# @!method ActiveRecord::Migration#change_domain(name, &block)
|
|
4
|
+
# Modify a domain type
|
|
5
|
+
#
|
|
6
|
+
# @param [#to_s] name (nil) The qualified name of the type
|
|
7
|
+
# @yield [Proc] the block with the type's definition
|
|
8
|
+
# @yieldparam The receiver of methods specifying the type
|
|
9
|
+
#
|
|
10
|
+
# The operation can be used to add or remove constraints,
|
|
11
|
+
# modify the default_sql value, or the description of the domain type.
|
|
12
|
+
# Neither the underlying type nor the collation can be changed.
|
|
13
|
+
#
|
|
14
|
+
# change_domain "dict.us_postal_code" do |d|
|
|
15
|
+
# d.null true # from: false
|
|
16
|
+
# # check is added for inversion
|
|
17
|
+
# d.drop_constraint "postal_code_length", check: <<~SQL
|
|
18
|
+
# length(VALUE) > 3 AND length(VALUE) < 6
|
|
19
|
+
# SQL
|
|
20
|
+
# d.add_constraint <<~SQL, name: "postal_code_valid"
|
|
21
|
+
# VALUE ~ '^\d{5}$' OR VALUE ~ '^\d{5}-\d{4}$'
|
|
22
|
+
# SQL
|
|
23
|
+
# d.default_sql "'00000'::text", from: "'0000'::text"
|
|
24
|
+
# d.comment <<~COMMENT, from: <<~COMMENT
|
|
25
|
+
# Supported currencies
|
|
26
|
+
# COMMENT
|
|
27
|
+
# Currencies
|
|
28
|
+
# COMMENT
|
|
29
|
+
# end
|
|
30
|
+
#
|
|
31
|
+
# Use blank string (not a `nil` value) to reset either a default_sql,
|
|
32
|
+
# or the comment. `nil`-s here will be ignored.
|
|
33
|
+
#
|
|
34
|
+
# When dropping a constraint you can use a `check` expression.
|
|
35
|
+
# In the same manner, use `from` option with `comment` or `default_sql`
|
|
36
|
+
# to make the operation invertible.
|
|
37
|
+
#
|
|
38
|
+
# It is irreversible in case any `drop_constraint` clause
|
|
39
|
+
# has `if_exists: true` or `force: :cascade` option -- due to
|
|
40
|
+
# uncertainty of the previous state of the database:
|
|
41
|
+
#
|
|
42
|
+
# # Irreversible change
|
|
43
|
+
# change_domain "dict.us_postal_code", force: :cascade do |d|
|
|
44
|
+
# d.drop_constraint "postal_code_valid" # missed `:check` option
|
|
45
|
+
# d.drop_constraint "postal_code_length"
|
|
46
|
+
# d.drop_constraint "postal_code_format", if_exists: true
|
|
47
|
+
# d.default_sql "'0000'::text" # missed `:from` option
|
|
48
|
+
# d.comment "New comment" # missed `:from` option
|
|
49
|
+
# end
|
|
50
|
+
|
|
51
|
+
module PGTrunk::Operations::Domains
|
|
52
|
+
# @private
|
|
53
|
+
class ChangeDomain < Base
|
|
54
|
+
# Methods to populate `constraints` from the block
|
|
55
|
+
|
|
56
|
+
def add_constraint(check, name: nil, valid: true)
|
|
57
|
+
constraints << Constraint.new(name: name, check: check, valid: valid)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def rename_constraint(name, to:)
|
|
61
|
+
constraints << Constraint.new(name: name, new_name: to)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def validate_constraint(name)
|
|
65
|
+
constraints << Constraint.new(name: name, valid: true)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def drop_constraint(name, check: nil, if_exists: nil)
|
|
69
|
+
constraints << Constraint.new(
|
|
70
|
+
check: check,
|
|
71
|
+
drop: true,
|
|
72
|
+
force: force,
|
|
73
|
+
if_exists: if_exists,
|
|
74
|
+
name: name,
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
validates :if_exists, :new_name, :type, :collation, absence: true
|
|
79
|
+
validate { errors.add :base, "There are no changes" if change.blank? }
|
|
80
|
+
|
|
81
|
+
def to_sql(_version)
|
|
82
|
+
[*change_default, *change_null, *change_constraints, *change_comment]
|
|
83
|
+
.join(" ")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def invert
|
|
87
|
+
keys = inversion.select { |_, v| v.nil? }.keys.join(", ").presence
|
|
88
|
+
errors = constraints.map(&:inversion_error).compact
|
|
89
|
+
errors << "Can't invert #{keys}" if keys.present?
|
|
90
|
+
raise IrreversibleMigration.new(self, nil, *errors) if errors.any?
|
|
91
|
+
|
|
92
|
+
self.class.new(**to_h, **inversion)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def change_default
|
|
98
|
+
<<~SQL.squish if default_sql
|
|
99
|
+
ALTER DOMAIN #{name.to_sql}
|
|
100
|
+
#{default_sql.present? ? "SET DEFAULT #{default_sql}" : 'DROP DEFAULT'};
|
|
101
|
+
SQL
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def change_null
|
|
105
|
+
<<~SQL.squish unless null.nil?
|
|
106
|
+
ALTER DOMAIN #{name.to_sql} #{null ? 'DROP' : 'SET'} NOT NULL;
|
|
107
|
+
SQL
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def change_constraints
|
|
111
|
+
constraints.map do |c|
|
|
112
|
+
"ALTER DOMAIN #{name.to_sql} #{c.to_sql};"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def change_comment
|
|
117
|
+
<<~SQL.squish if comment
|
|
118
|
+
COMMENT ON DOMAIN #{name.to_sql} IS $comment$#{comment}$comment$;
|
|
119
|
+
SQL
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def change
|
|
123
|
+
@change ||= {
|
|
124
|
+
comment: comment,
|
|
125
|
+
constraints: constraints.map(&:to_h).presence,
|
|
126
|
+
default_sql: default_sql,
|
|
127
|
+
null: null,
|
|
128
|
+
}.compact
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def inversion
|
|
132
|
+
@inversion ||= {
|
|
133
|
+
comment: from_comment,
|
|
134
|
+
constraints: constraints&.map(&:invert),
|
|
135
|
+
default_sql: from_default_sql,
|
|
136
|
+
null: !null,
|
|
137
|
+
}.slice(*change.keys)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: false
|
|
2
|
+
|
|
3
|
+
module PGTrunk::Operations::Domains
|
|
4
|
+
# @private
|
|
5
|
+
# Definition for the domain's constraint
|
|
6
|
+
class Constraint
|
|
7
|
+
include ActiveModel::Model
|
|
8
|
+
include ActiveModel::Attributes
|
|
9
|
+
include ActiveModel::Validations
|
|
10
|
+
|
|
11
|
+
def self.build(data)
|
|
12
|
+
data.is_a?(self) ? data : new(**data)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
attribute :check, :pg_trunk_multiline_text
|
|
16
|
+
attribute :drop, :boolean
|
|
17
|
+
attribute :force, :pg_trunk_symbol
|
|
18
|
+
attribute :if_exists, :boolean
|
|
19
|
+
attribute :name, :string
|
|
20
|
+
attribute :new_name, :string
|
|
21
|
+
attribute :valid, :boolean
|
|
22
|
+
|
|
23
|
+
validates :name, presence: true
|
|
24
|
+
validates :new_name, "PGTrunk/difference": { from: :name }, allow_nil: true
|
|
25
|
+
validates :force, inclusion: { in: %i[force restrict] }, allow_nil: true
|
|
26
|
+
|
|
27
|
+
def to_h
|
|
28
|
+
@to_h ||= attributes.compact.symbolize_keys
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def opts
|
|
32
|
+
to_h.slice(:name)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def invert
|
|
36
|
+
@invert ||= {}.tap do |i|
|
|
37
|
+
i[:name] = new_name.presence || name
|
|
38
|
+
i[:new_name] = name if new_name.present?
|
|
39
|
+
i[:drop] = !drop if new_name.blank?
|
|
40
|
+
i[:check] = check if drop
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def to_sql
|
|
45
|
+
rename_sql || drop_sql || add_sql || validate_sql
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def inversion_error
|
|
49
|
+
return <<~MSG.squish if if_exists
|
|
50
|
+
with `if_exists: true` option cannot be inverted
|
|
51
|
+
due to uncertainty of the previous state of the database.
|
|
52
|
+
MSG
|
|
53
|
+
|
|
54
|
+
return <<~MSG.squish if force == :cascade
|
|
55
|
+
with `force: :cascade` option cannot be inverted
|
|
56
|
+
due to uncertainty of the previous state of the database.
|
|
57
|
+
MSG
|
|
58
|
+
|
|
59
|
+
return if check.present?
|
|
60
|
+
|
|
61
|
+
"the constraint `#{name}` is dropped without `check` option."
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def rename_sql
|
|
67
|
+
<<~SQL.squish if new_name.present?
|
|
68
|
+
RENAME CONSTRAINT #{name.inspect} TO #{new_name.inspect}
|
|
69
|
+
SQL
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def drop_sql
|
|
73
|
+
return unless drop
|
|
74
|
+
|
|
75
|
+
sql = "DROP CONSTRAINT"
|
|
76
|
+
sql << " IF EXISTS" if if_exists
|
|
77
|
+
sql << " #{name.inspect}"
|
|
78
|
+
sql << " CASCADE" if force == :cascade
|
|
79
|
+
sql << ";"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def add_sql
|
|
83
|
+
<<~SQL.squish if check.present?
|
|
84
|
+
ADD CONSTRAINT #{name.inspect}
|
|
85
|
+
CHECK (#{check})#{' NOT VALID' unless valid}
|
|
86
|
+
SQL
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def validate_sql
|
|
90
|
+
"VALIDATE CONSTRAINT #{name.inspect}" if valid
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: false
|
|
2
|
+
|
|
3
|
+
# @!method ActiveRecord::Migration#create_domain(name, **options, &block)
|
|
4
|
+
# Create a domain type
|
|
5
|
+
#
|
|
6
|
+
# @param [#to_s] name (nil) The qualified name of the type
|
|
7
|
+
# @option [#to_s] :as (nil) The base type for the domain (alias: :type)
|
|
8
|
+
# @option [#to_s] :collation (nil) The collation
|
|
9
|
+
# @option [Boolean] :null (true) If a value of this type can be NULL
|
|
10
|
+
# @option [#to_s] :default_sql (nil) The snippet for the default value of the domain
|
|
11
|
+
# @option [#to_s] :comment (nil) The comment describing the constraint
|
|
12
|
+
# @yield [Proc] the block with the type's definition
|
|
13
|
+
# @yieldparam The receiver of methods specifying the type
|
|
14
|
+
#
|
|
15
|
+
# @example:
|
|
16
|
+
#
|
|
17
|
+
# create_domain "dict.us_postal_code", as: "text" do |d|
|
|
18
|
+
# d.collation "en_US"
|
|
19
|
+
# d.default_sql "'0000'::text"
|
|
20
|
+
# d.null false
|
|
21
|
+
# d.constraint <<~SQL, name: "code_valid"
|
|
22
|
+
# VALUE ~ '^\d{5}$' OR VALUE ~ '^\d{5}-\d{4}$'
|
|
23
|
+
# SQL
|
|
24
|
+
# d.comment "US postal code"
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# It is always reversible.
|
|
28
|
+
|
|
29
|
+
module PGTrunk::Operations::Domains
|
|
30
|
+
# @private
|
|
31
|
+
class CreateDomain < Base
|
|
32
|
+
validates :type, presence: true
|
|
33
|
+
validates :force, :if_exists, :new_name, absence: true
|
|
34
|
+
|
|
35
|
+
from_sql do |_version|
|
|
36
|
+
<<~SQL
|
|
37
|
+
SELECT
|
|
38
|
+
t.oid,
|
|
39
|
+
(t.typnamespace::regnamespace || '.' || t.typname) AS name,
|
|
40
|
+
(
|
|
41
|
+
CASE
|
|
42
|
+
WHEN b.typnamespace != 'pg_catalog'::regnamespace
|
|
43
|
+
THEN b.typnamespace::regnamespace || '.' || b.typname
|
|
44
|
+
ELSE b.typname
|
|
45
|
+
END
|
|
46
|
+
) AS "type",
|
|
47
|
+
(
|
|
48
|
+
CASE
|
|
49
|
+
WHEN c.collnamespace != 'pg_catalog'::regnamespace
|
|
50
|
+
THEN c.collnamespace::regnamespace || '.' || c.collname
|
|
51
|
+
WHEN c.collname != 'default'
|
|
52
|
+
THEN c.collname
|
|
53
|
+
END
|
|
54
|
+
) AS collation,
|
|
55
|
+
(CASE WHEN t.typnotnull THEN false END) AS null,
|
|
56
|
+
(
|
|
57
|
+
CASE
|
|
58
|
+
WHEN t.typdefaultbin IS NOT NULL
|
|
59
|
+
THEN pg_get_expr(t.typdefaultbin, 0, true)
|
|
60
|
+
END
|
|
61
|
+
) AS default_sql,
|
|
62
|
+
(
|
|
63
|
+
SELECT json_agg(
|
|
64
|
+
json_build_object(
|
|
65
|
+
'name', c.conname,
|
|
66
|
+
'check', pg_get_expr(c.conbin, 0, true)
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
FROM pg_constraint c
|
|
70
|
+
WHERE c.contypid = t.oid
|
|
71
|
+
) AS constraints,
|
|
72
|
+
d.description AS comment
|
|
73
|
+
FROM pg_type t
|
|
74
|
+
JOIN pg_trunk e ON e.oid = t.oid
|
|
75
|
+
AND e.classid = 'pg_type'::regclass
|
|
76
|
+
JOIN pg_type b ON b.oid = t.typbasetype
|
|
77
|
+
LEFT JOIN pg_collation c ON c.oid = t.typcollation
|
|
78
|
+
LEFT JOIN pg_description d ON d.objoid = t.oid
|
|
79
|
+
AND d.classoid = 'pg_type'::regclass
|
|
80
|
+
WHERE t.typtype = 'd';
|
|
81
|
+
SQL
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def to_sql(_version)
|
|
85
|
+
[create_domain, *create_comment, register_domain].join(" ")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def invert
|
|
89
|
+
DropDomain.new(**to_h)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def create_domain
|
|
95
|
+
sql = "CREATE DOMAIN #{name.to_sql} AS #{type.to_sql}"
|
|
96
|
+
sql << " COLLATE #{collation.to_sql}" if collation.present?
|
|
97
|
+
sql << " DEFAULT #{default_sql}" if default_sql.present?
|
|
98
|
+
sql << " NOT NULL" if null == false
|
|
99
|
+
constraints&.each do |c|
|
|
100
|
+
sql << " CONSTRAINT #{c.name.inspect}" if c.name.present?
|
|
101
|
+
sql << " CHECK (#{c.check})"
|
|
102
|
+
end
|
|
103
|
+
sql << ";"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def create_comment
|
|
107
|
+
return if comment.blank?
|
|
108
|
+
|
|
109
|
+
"COMMENT ON DOMAIN #{name.to_sql} IS $comment$#{comment}$comment$;"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def register_domain
|
|
113
|
+
<<~SQL.squish
|
|
114
|
+
INSERT INTO pg_trunk (oid, classid)
|
|
115
|
+
SELECT oid, 'pg_type'::regclass
|
|
116
|
+
FROM pg_type
|
|
117
|
+
WHERE typname = #{name.quoted}
|
|
118
|
+
AND typnamespace = #{name.namespace}
|
|
119
|
+
AND typtype = 'd'
|
|
120
|
+
ON CONFLICT DO NOTHING;
|
|
121
|
+
SQL
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: false
|
|
2
|
+
|
|
3
|
+
# @!method ActiveRecord::Migration#drop_domain(name, **options, &block)
|
|
4
|
+
# Drop a domain type by qualified name
|
|
5
|
+
#
|
|
6
|
+
# @param [#to_s] name (nil) The qualified name of the type
|
|
7
|
+
# @option [Boolean] :if_exists (false) Suppress the error when the type is absent
|
|
8
|
+
# @option [Symbol] :force (:restrict) How to process dependent objects (`:cascade` or `:restrict`)
|
|
9
|
+
# @option [#to_s] :as (nil) The base type for the domain (alias: :type)
|
|
10
|
+
# @option [#to_s] :collation (nil) The collation
|
|
11
|
+
# @option [#to_s] :default_sql (nil) The snippet for the default value of the domain
|
|
12
|
+
# @option [#to_s] :comment (nil) The comment describing the constraint
|
|
13
|
+
# @yield [Proc] the block with the type's definition
|
|
14
|
+
# @yieldparam The receiver of methods specifying the type
|
|
15
|
+
#
|
|
16
|
+
# @example:
|
|
17
|
+
#
|
|
18
|
+
# drop_domain "dict.us_postal_code"
|
|
19
|
+
#
|
|
20
|
+
# To make the operation invertible, use the same options
|
|
21
|
+
# as in the `create_domain` operation.
|
|
22
|
+
#
|
|
23
|
+
# drop_domain "dict.us_postal_code", as: "string" do |d|
|
|
24
|
+
# d.constraint <<~SQL, name: "code_valid"
|
|
25
|
+
# VALUE ~ '^\d{5}$' OR VALUE ~ '^\d{5}-\d{4}$'
|
|
26
|
+
# SQL
|
|
27
|
+
# d.comment <<~COMMENT
|
|
28
|
+
# US postal code
|
|
29
|
+
# COMMENT
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
# With the `force: :cascade` option the operation would remove
|
|
33
|
+
# all the objects that use the type.
|
|
34
|
+
#
|
|
35
|
+
# drop_domain "dict.us_postal_code", force: :cascade
|
|
36
|
+
#
|
|
37
|
+
# With the `if_exists: true` option the operation won't fail
|
|
38
|
+
# even when the view was absent in the database.
|
|
39
|
+
#
|
|
40
|
+
# drop_domain "dict.us_postal_code", if_exists: true
|
|
41
|
+
#
|
|
42
|
+
# Both options make a migration irreversible due to uncertainty
|
|
43
|
+
# of the previous state of the database.
|
|
44
|
+
|
|
45
|
+
module PGTrunk::Operations::Domains
|
|
46
|
+
# @private
|
|
47
|
+
class DropDomain < Base
|
|
48
|
+
# Forbid these attributes
|
|
49
|
+
validates :new_name, absence: true
|
|
50
|
+
|
|
51
|
+
def to_sql(_version)
|
|
52
|
+
sql = "DROP DOMAIN"
|
|
53
|
+
sql << " IF EXISTS" if if_exists
|
|
54
|
+
sql << " #{name.to_sql}"
|
|
55
|
+
sql << " CASCADE" if force == :cascade
|
|
56
|
+
sql << ";"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def invert
|
|
60
|
+
irreversible!("if_exists: true") if if_exists
|
|
61
|
+
irreversible!("force: :cascade") if force == :cascade
|
|
62
|
+
CreateDomain.new(**to_h.except(:force))
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: false
|
|
2
|
+
|
|
3
|
+
# @!method ActiveRecord::Migration#rename_domain(name, to:)
|
|
4
|
+
# Change the name and/or schema of a domain type
|
|
5
|
+
#
|
|
6
|
+
# @param [#to_s] :name (nil) The qualified name of the type
|
|
7
|
+
# @option [#to_s] :to (nil) The new qualified name for the type
|
|
8
|
+
#
|
|
9
|
+
# A domain type can be both renamed and moved to another schema.
|
|
10
|
+
#
|
|
11
|
+
# rename_domain "us_code", to: "dict.us_postal_code"
|
|
12
|
+
#
|
|
13
|
+
# The operation is always reversible.
|
|
14
|
+
|
|
15
|
+
module PGTrunk::Operations::Domains
|
|
16
|
+
# @private
|
|
17
|
+
class RenameDomain < Base
|
|
18
|
+
validates :new_name, presence: true
|
|
19
|
+
validates :force, :if_exists, absence: true
|
|
20
|
+
|
|
21
|
+
def to_sql(_version)
|
|
22
|
+
[*change_schema, *change_name].join("; ")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def invert
|
|
26
|
+
self.class.new(**to_h, name: new_name, to: name)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def change_schema
|
|
32
|
+
return if name.schema == new_name.schema
|
|
33
|
+
|
|
34
|
+
"ALTER DOMAIN #{name.to_sql} SET SCHEMA #{new_name.schema.inspect};"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def change_name
|
|
38
|
+
return if new_name.name == name.name
|
|
39
|
+
|
|
40
|
+
moved = name.merge(schema: new_name.schema)
|
|
41
|
+
"ALTER DOMAIN #{moved.to_sql} RENAME TO #{new_name.name.inspect};"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# nodoc
|
|
4
|
+
module PGTrunk::Operations
|
|
5
|
+
# @private
|
|
6
|
+
# Namespace for operations with functions
|
|
7
|
+
module Domains
|
|
8
|
+
require_relative "domains/constraint"
|
|
9
|
+
require_relative "domains/base"
|
|
10
|
+
require_relative "domains/change_domain"
|
|
11
|
+
require_relative "domains/create_domain"
|
|
12
|
+
require_relative "domains/drop_domain"
|
|
13
|
+
require_relative "domains/rename_domain"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: false
|
|
2
|
+
|
|
3
|
+
module PGTrunk::Operations::Enums
|
|
4
|
+
# @abstract
|
|
5
|
+
# @private
|
|
6
|
+
# Base class for operations with enumerated types
|
|
7
|
+
class Base < PGTrunk::Operation
|
|
8
|
+
# All attributes that can be used by enum-related commands
|
|
9
|
+
attribute :changes, :pg_trunk_array_of_hashes, default: []
|
|
10
|
+
attribute :values, :pg_trunk_array_of_strings, default: []
|
|
11
|
+
|
|
12
|
+
# populate values one-by-one in a block
|
|
13
|
+
def value(value)
|
|
14
|
+
values << value.to_s
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# wrap change definitions into value objects
|
|
18
|
+
after_initialize { changes.map! { |change| Change.build(change) } }
|
|
19
|
+
|
|
20
|
+
validates :if_not_exists, absence: true
|
|
21
|
+
validates :name, presence: true
|
|
22
|
+
validates :changes, "PGTrunk/all_items_valid": true, allow_nil: true
|
|
23
|
+
|
|
24
|
+
# Use comparison by name from pg_trunk operations base class (default)
|
|
25
|
+
# Support name as the only positional argument (default)
|
|
26
|
+
|
|
27
|
+
ruby_snippet do |s|
|
|
28
|
+
s.ruby_param(name.lean) if name.present?
|
|
29
|
+
s.ruby_param(to: new_name) if new_name.present?
|
|
30
|
+
s.ruby_param(if_exists: true) if if_exists
|
|
31
|
+
s.ruby_param(force: :cascade) if force == :cascade
|
|
32
|
+
|
|
33
|
+
if values.join(", ").length < 60
|
|
34
|
+
s.ruby_line(:values, *values)
|
|
35
|
+
else
|
|
36
|
+
values.each { |value| s.ruby_line(:value, value) }
|
|
37
|
+
end
|
|
38
|
+
changes.select(&:add?).each do |change|
|
|
39
|
+
s.ruby_line(:add_value, change.name, **change.opts)
|
|
40
|
+
end
|
|
41
|
+
changes.select(&:rename?).each do |change|
|
|
42
|
+
s.ruby_line(:rename_value, change.name, to: change.new_name)
|
|
43
|
+
end
|
|
44
|
+
s.ruby_line(:comment, comment) if comment.present?
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: false
|
|
2
|
+
|
|
3
|
+
module PGTrunk::Operations::Enums
|
|
4
|
+
# @private
|
|
5
|
+
# Definition for the value's change
|
|
6
|
+
class Change
|
|
7
|
+
include ActiveModel::Model
|
|
8
|
+
include ActiveModel::Attributes
|
|
9
|
+
include ActiveModel::Validations
|
|
10
|
+
|
|
11
|
+
def self.build(data)
|
|
12
|
+
data.is_a?(self) ? data : new(**data)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
attribute :name, :string
|
|
16
|
+
attribute :new_name, :string
|
|
17
|
+
attribute :after, :string
|
|
18
|
+
attribute :before, :string
|
|
19
|
+
|
|
20
|
+
validates :name, presence: true
|
|
21
|
+
validates :new_name, "PGTrunk/difference": { from: :name }, allow_nil: true
|
|
22
|
+
validate { errors.add :after, :present if rename? && after.present? }
|
|
23
|
+
validate { errors.add :before, :present if rename? && before.present? }
|
|
24
|
+
validate { errors.add :before, :present if [after, before].all?(&:present?) }
|
|
25
|
+
|
|
26
|
+
def rename?
|
|
27
|
+
new_name.present?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def add?
|
|
31
|
+
new_name.blank?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def to_h
|
|
35
|
+
attributes.compact.symbolize_keys
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def opts
|
|
39
|
+
to_h.slice(:before, :after).compact
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def invert
|
|
43
|
+
{ name: new_name, new_name: name }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def to_sql
|
|
47
|
+
return "RENAME VALUE '#{name}' TO '#{new_name}'" if new_name.present?
|
|
48
|
+
|
|
49
|
+
sql = "ADD VALUE IF NOT EXISTS $value$#{name}$value$"
|
|
50
|
+
sql << " BEFORE $value$#{before}$value$" if before.present?
|
|
51
|
+
sql << " AFTER $value$#{after}$value$" if after.present?
|
|
52
|
+
sql
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|