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,181 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
|
3
|
+
# @!method ActiveRecord::Migration#create_statistics(name, **options, &block)
|
4
|
+
# Create a custom statistics
|
5
|
+
#
|
6
|
+
# @param [#to_s] name (nil) The qualified name of the statistics
|
7
|
+
# @option [Boolean] :if_not_exists (false)
|
8
|
+
# Suppress the error when the statistics is already exist
|
9
|
+
# @option [#to_s] table (nil)
|
10
|
+
# The qualified name of the table whose statistics will be collected
|
11
|
+
# @option [Array<Symbol>] kinds ([:dependencies, :mcv, :ndistinct])
|
12
|
+
# The kinds of statistics to be collected (all by default).
|
13
|
+
# Supported values in the array: :dependencies, :mcv, :ndistinct
|
14
|
+
# @option [#to_s] :comment The description of the statistics
|
15
|
+
# @yield [Proc] the block with the statistics' definition
|
16
|
+
# @yieldparam The receiver of methods specifying the statistics
|
17
|
+
#
|
18
|
+
# The statistics can be created with explicit name:
|
19
|
+
#
|
20
|
+
# create_statistics "users_stats" do |s|
|
21
|
+
# s.table "users"
|
22
|
+
# s.columns "family", "name"
|
23
|
+
# s.kinds :dependencies, :mcv, :ndistinct
|
24
|
+
# s.comment "Statistics for users' names and families"
|
25
|
+
# SQL
|
26
|
+
#
|
27
|
+
# The name can be generated as well:
|
28
|
+
#
|
29
|
+
# create_statistics do |s|
|
30
|
+
# s.table "users"
|
31
|
+
# s.columns "family", "name"
|
32
|
+
# s.kinds :dependencies, :mcv, :ndistinct
|
33
|
+
# s.comment "Statistics for users' names and families"
|
34
|
+
# SQL
|
35
|
+
#
|
36
|
+
# Since v14 PostgreSQL have supported expressions in addition to columns:
|
37
|
+
#
|
38
|
+
# create_statistics "users_stats" do |s|
|
39
|
+
# s.table "users"
|
40
|
+
# s.columns "family"
|
41
|
+
# s.expression "length(name)"
|
42
|
+
# s.kinds :dependencies, :mcv, :ndistinct
|
43
|
+
# s.comment "Statistics for users' name lengths and families"
|
44
|
+
# SQL
|
45
|
+
#
|
46
|
+
# as well as statistics for the sole expression (kinds must be blank)
|
47
|
+
# by columns of some table.
|
48
|
+
#
|
49
|
+
# create_statistics "users_stats" do |s|
|
50
|
+
# s.table "users"
|
51
|
+
# s.expression "length(name || ' ' || family)"
|
52
|
+
# s.comment "Statistics for full name lengths"
|
53
|
+
# SQL
|
54
|
+
#
|
55
|
+
# Use `if_not_exists: true` to suppress error in case the statistics
|
56
|
+
# has already been created. This option, though, makes the migration
|
57
|
+
# irreversible due to uncertainty of the previous state of the database.
|
58
|
+
|
59
|
+
module PGTrunk::Operations::Statistics
|
60
|
+
# SQL snippet to fetch statistics in v10-13
|
61
|
+
SQL_V10 = <<~SQL.freeze
|
62
|
+
WITH
|
63
|
+
list (key, name) AS (
|
64
|
+
VALUES ('m', 'mcv'), ('f', 'dependencies'), ('d', 'ndistinct')
|
65
|
+
)
|
66
|
+
SELECT
|
67
|
+
s.oid,
|
68
|
+
(s.stxnamespace::regnamespace || '.' || s.stxname) AS name,
|
69
|
+
(t.relnamespace::regnamespace || '.' || t.relname) AS "table",
|
70
|
+
(
|
71
|
+
SELECT array_agg(l.name)
|
72
|
+
FROM list l
|
73
|
+
WHERE ARRAY[l.key]::char[] <@ s.stxkind::char[]
|
74
|
+
) AS kinds,
|
75
|
+
(
|
76
|
+
SELECT array_agg(DISTINCT a.attname)
|
77
|
+
FROM pg_attribute a
|
78
|
+
WHERE a.attrelid = s.stxrelid
|
79
|
+
AND ARRAY[a.attnum]::int[] <@ s.stxkeys::int[]
|
80
|
+
) AS columns,
|
81
|
+
d.description AS comment
|
82
|
+
FROM pg_statistic_ext s
|
83
|
+
JOIN pg_trunk e ON e.oid = s.oid AND e.classid = 'pg_statistic_ext'::regclass
|
84
|
+
JOIN pg_class t ON t.oid = s.stxrelid
|
85
|
+
LEFT JOIN pg_description d ON d.objoid = s.oid;
|
86
|
+
SQL
|
87
|
+
|
88
|
+
# In version 14 statistics can be collected for expressions.
|
89
|
+
SQL_V14 = <<~SQL.freeze
|
90
|
+
WITH
|
91
|
+
list (key, name) AS (
|
92
|
+
VALUES ('m', 'mcv'), ('f', 'dependencies'), ('d', 'ndistinct')
|
93
|
+
)
|
94
|
+
SELECT
|
95
|
+
s.oid,
|
96
|
+
(s.stxnamespace::regnamespace || '.' || s.stxname) AS name,
|
97
|
+
(t.relnamespace::regnamespace || '.' || t.relname) AS "table",
|
98
|
+
(
|
99
|
+
SELECT array_agg(l.name)
|
100
|
+
FROM list l
|
101
|
+
WHERE ARRAY[l.key]::char[] <@ s.stxkind::char[]
|
102
|
+
) AS kinds,
|
103
|
+
(
|
104
|
+
SELECT array_agg(DISTINCT a.attname)
|
105
|
+
FROM pg_attribute a
|
106
|
+
WHERE a.attrelid = s.stxrelid
|
107
|
+
AND ARRAY[a.attnum]::int[] <@ s.stxkeys::int[]
|
108
|
+
) AS columns,
|
109
|
+
pg_get_expr(s.stxexprs, stxrelid, true) AS expressions,
|
110
|
+
d.description AS comment
|
111
|
+
FROM pg_statistic_ext s
|
112
|
+
JOIN pg_trunk e ON e.oid = s.oid AND e.classid = 'pg_statistic_ext'::regclass
|
113
|
+
JOIN pg_class t ON t.oid = s.stxrelid
|
114
|
+
LEFT JOIN pg_description d ON d.objoid = s.oid;
|
115
|
+
SQL
|
116
|
+
|
117
|
+
# @private
|
118
|
+
class CreateStatistics < Base
|
119
|
+
validates :if_exists, :force, :new_name, absence: true
|
120
|
+
validates :table, presence: true
|
121
|
+
validate do
|
122
|
+
errors.add :base, "Columns and expressions can't be blank" if parts.blank?
|
123
|
+
end
|
124
|
+
|
125
|
+
from_sql do |version|
|
126
|
+
version >= "14" ? SQL_V14 : SQL_V10
|
127
|
+
end
|
128
|
+
|
129
|
+
def to_sql(version)
|
130
|
+
check_version!(version)
|
131
|
+
|
132
|
+
[create_statistics, *create_comment, register_object].join(" ")
|
133
|
+
end
|
134
|
+
|
135
|
+
def invert
|
136
|
+
irreversible!("if_not_exists: true") if if_not_exists
|
137
|
+
DropStatistics.new(**to_h)
|
138
|
+
end
|
139
|
+
|
140
|
+
private
|
141
|
+
|
142
|
+
def check_version!(version)
|
143
|
+
raise <<~ERROR.squish if version < "14" && expressions.present?
|
144
|
+
Statistics for expressions are supported in PostgreSQL v14+"
|
145
|
+
ERROR
|
146
|
+
|
147
|
+
raise <<~ERROR.squish if version < "12" && kinds.include?(:mcv)
|
148
|
+
The `mcv` kind is supported in PostgreSQL v12+
|
149
|
+
ERROR
|
150
|
+
end
|
151
|
+
|
152
|
+
def create_statistics
|
153
|
+
sql = "CREATE STATISTICS"
|
154
|
+
sql << " IF NOT EXISTS" if if_not_exists
|
155
|
+
sql << " #{name.to_sql}"
|
156
|
+
sql << " (#{kinds.join(',')})" if kinds.present?
|
157
|
+
sql << " ON #{parts.join(', ')}"
|
158
|
+
sql << "FROM #{table.to_sql};"
|
159
|
+
end
|
160
|
+
|
161
|
+
def create_comment
|
162
|
+
return if comment.blank?
|
163
|
+
|
164
|
+
"COMMENT ON STATISTICS #{name.to_sql} IS $comment$#{comment}$comment$;"
|
165
|
+
end
|
166
|
+
|
167
|
+
def register_object
|
168
|
+
<<~SQL
|
169
|
+
INSERT INTO pg_trunk(oid, classid)
|
170
|
+
SELECT s.oid, 'pg_statistic_ext'::regclass
|
171
|
+
FROM pg_statistic_ext s
|
172
|
+
JOIN pg_class t ON t.oid = s.stxrelid
|
173
|
+
WHERE s.stxname = #{name.quoted}
|
174
|
+
AND s.stxnamespace = #{name.namespace}
|
175
|
+
AND t.relname = #{table.quoted}
|
176
|
+
AND t.relnamespace = #{table.namespace}
|
177
|
+
ON CONFLICT DO NOTHING;
|
178
|
+
SQL
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
|
3
|
+
# @!method ActiveRecord::Migration#drop_statistics(name, **options, &block)
|
4
|
+
# Drop a custom statistics
|
5
|
+
#
|
6
|
+
# @param [#to_s] name (nil) The qualified name of the statistics
|
7
|
+
# @option [Boolean] :if_exists (false) Suppress the error when the statistics is absent
|
8
|
+
# @option [Symbol] :force (:restrict) How to process dependent objects (`:cascade` or `:restrict`)
|
9
|
+
# @option [#to_s] table (nil)
|
10
|
+
# The qualified name of the table whose statistics will be collected
|
11
|
+
# @option [Array<Symbol>] kinds ([:dependencies, :mcv, :ndistinct])
|
12
|
+
# The kinds of statistics to be collected (all by default).
|
13
|
+
# Supported values in the array: :dependencies, :mcv, :ndistinct
|
14
|
+
# @option [#to_s] :comment The description of the statistics
|
15
|
+
# @yield [Proc] the block with the statistics' definition
|
16
|
+
# @yieldparam The receiver of methods specifying the statistics
|
17
|
+
#
|
18
|
+
# A statistics can be dropped by its name only:
|
19
|
+
#
|
20
|
+
# drop_statistics "my_stats"
|
21
|
+
#
|
22
|
+
# Such operation is irreversible. To make it inverted
|
23
|
+
# you have to provide a full definition:
|
24
|
+
#
|
25
|
+
# drop_statistics "users_stat" do |s|
|
26
|
+
# s.table "users"
|
27
|
+
# s.columns "firstname", "name"
|
28
|
+
# s.expression <<~SQL
|
29
|
+
# round(age, 10)
|
30
|
+
# SQL
|
31
|
+
# s.kinds :dependency, :mcv, :ndistinct
|
32
|
+
# p.comment "Statistics for name, firstname, and rough age"
|
33
|
+
# SQL
|
34
|
+
#
|
35
|
+
# If the statistics was anonymous (used the generated name),
|
36
|
+
# it can be dropped without defining the name as well:
|
37
|
+
#
|
38
|
+
# drop_statistics do |s|
|
39
|
+
# s.table "users"
|
40
|
+
# s.columns "firstname", "name"
|
41
|
+
# s.expression <<~SQL
|
42
|
+
# round(age, 10)
|
43
|
+
# SQL
|
44
|
+
# s.kinds :dependency, :mcv, :ndistinct
|
45
|
+
# p.comment "Statistics for name, firstname, and rough age"
|
46
|
+
# SQL
|
47
|
+
#
|
48
|
+
# The operation can be called with `if_exists` option. In this case
|
49
|
+
# it would do nothing when no statistics existed.
|
50
|
+
#
|
51
|
+
# drop_procedure "unknown_statistics", if_exists: true
|
52
|
+
#
|
53
|
+
# Notice, that this option make the operation irreversible because of
|
54
|
+
# uncertainty about the previous state of the database.
|
55
|
+
|
56
|
+
module PGTrunk::Operations::Statistics
|
57
|
+
# @private
|
58
|
+
class DropStatistics < Base
|
59
|
+
validates :if_not_exists, :new_name, absence: true
|
60
|
+
|
61
|
+
def to_sql(_version)
|
62
|
+
sql = "DROP STATISTICS"
|
63
|
+
sql << " IF EXISTS" if if_exists
|
64
|
+
sql << " #{name.to_sql}"
|
65
|
+
sql << " CASCADE" if force == :cascade
|
66
|
+
sql << ";"
|
67
|
+
end
|
68
|
+
|
69
|
+
def invert
|
70
|
+
irreversible!("if_exists: true") if if_exists
|
71
|
+
irreversible!("force: :cascade") if force == :cascade
|
72
|
+
CreateStatistics.new(**to_h.except(:force))
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
|
3
|
+
# @!method ActiveRecord::Migration#rename_statistics(name, to:)
|
4
|
+
# Change the name and/or schema of a statistics
|
5
|
+
#
|
6
|
+
# @param [#to_s] :name (nil) The qualified name of the statistics
|
7
|
+
# @option [#to_s] :to (nil) The new qualified name for the statistics
|
8
|
+
#
|
9
|
+
# A custom statistics can be renamed by changing both the name
|
10
|
+
# and the schema (namespace) it belongs to.
|
11
|
+
#
|
12
|
+
# rename_statistics "math.my_stat", to: "public.my_stats"
|
13
|
+
#
|
14
|
+
# The operation is always reversible.
|
15
|
+
|
16
|
+
module PGTrunk::Operations::Statistics
|
17
|
+
# @private
|
18
|
+
class RenameStatistics < Base
|
19
|
+
after_initialize { self.new_name ||= generated_name }
|
20
|
+
|
21
|
+
validates :new_name, presence: true
|
22
|
+
validates :if_exists, :if_not_exists, :force, absence: true
|
23
|
+
|
24
|
+
def to_sql(_version)
|
25
|
+
[*change_schema, *change_name].join("; ")
|
26
|
+
end
|
27
|
+
|
28
|
+
def invert
|
29
|
+
q_new_name = "#{new_name.schema}.#{new_name.routine}(#{name.args}) #{name.returns}"
|
30
|
+
self.class.new(**to_h, name: q_new_name.strip, to: name)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def change_schema
|
36
|
+
return if name.schema == new_name.schema
|
37
|
+
|
38
|
+
"ALTER STATISTICS #{name.to_sql} SET SCHEMA #{new_name.schema.inspect};"
|
39
|
+
end
|
40
|
+
|
41
|
+
def change_name
|
42
|
+
return if new_name.routine == name.routine
|
43
|
+
|
44
|
+
changed_name = name.merge(schema: new_name.schema).to_sql
|
45
|
+
"ALTER STATISTICS #{changed_name} RENAME TO #{new_name.routine.inspect};"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# nodoc
|
4
|
+
module PGTrunk::Operations
|
5
|
+
# @private
|
6
|
+
# Namespace for operations with functions
|
7
|
+
module Statistics
|
8
|
+
require_relative "statistics/base"
|
9
|
+
require_relative "statistics/create_statistics"
|
10
|
+
require_relative "statistics/drop_statistics"
|
11
|
+
require_relative "statistics/rename_statistics"
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PGTrunk::Operations::Tables
|
4
|
+
# @private
|
5
|
+
#
|
6
|
+
# When dealing with tables we're only interested in
|
7
|
+
# dumping tables one-by-one to enable other operations
|
8
|
+
# in between tables.
|
9
|
+
#
|
10
|
+
# We doesn't overload the method `create_table`, but
|
11
|
+
# keep the original implementation unchanged. That's why
|
12
|
+
# neither `to_sql`, `invert` or `generates_object` are necessary.
|
13
|
+
#
|
14
|
+
# While we rely on the original implementation,
|
15
|
+
# there are some differences in a way we fetching
|
16
|
+
# tables and dumping them to the schema:
|
17
|
+
#
|
18
|
+
# - we extracting both qualified +name+ and +oid+ for every table,
|
19
|
+
# and checking them against the content of `pg_trunk`;
|
20
|
+
# - we wrap every table new_name this class for dependencies resolving;
|
21
|
+
# - we don't keep indexes and check constraints
|
22
|
+
# inside the table definitions because they can depend
|
23
|
+
# on functions which, in turn, can depend on tables.
|
24
|
+
#
|
25
|
+
class CreateTable < PGTrunk::Operation
|
26
|
+
# No other attributes except for the mandatory `name` and `oid` are needed.
|
27
|
+
# We also use default ordering by qualified names.
|
28
|
+
validates :oid, presence: true
|
29
|
+
|
30
|
+
# SQL to fetch table names and oids from the database.
|
31
|
+
# We rely on the fact all tables of interest are registered in `pg_trunk`.
|
32
|
+
from_sql do
|
33
|
+
<<~SQL
|
34
|
+
SELECT
|
35
|
+
c.oid,
|
36
|
+
(c.relnamespace::regnamespace || '.' || c.relname) AS name
|
37
|
+
FROM pg_class c JOIN pg_trunk p ON p.oid = c.oid
|
38
|
+
-- 'r' for tables and 'p' for partitions
|
39
|
+
WHERE c.relkind IN ('r', 'p')
|
40
|
+
SQL
|
41
|
+
end
|
42
|
+
|
43
|
+
# Instead of defining +ruby_snippet+, we overload
|
44
|
+
# the +to_ruby+ to rely on the original implementation.
|
45
|
+
#
|
46
|
+
# We overloaded the +ActiveRecord::SchemaDumper+
|
47
|
+
# methods +indexes_in_create+ and +check_constraints_in_create+
|
48
|
+
# so that they do nothing to exclude indexes and constraints
|
49
|
+
# from a table definition.
|
50
|
+
#
|
51
|
+
# @see +PGTrunk::SchemaDumper+ module (in `core/railtie`).
|
52
|
+
def to_ruby
|
53
|
+
stream = StringIO.new
|
54
|
+
PGTrunk.dumper.send(:table, name.lean, stream)
|
55
|
+
unindent(stream.string)
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
# ActiveRecord builds the dump indented by 2 space chars.
|
61
|
+
# Because the +to_ruby+ method is used in error messages,
|
62
|
+
# we do indentation separately in the +PGTrunk::SchemaDumper+.
|
63
|
+
#
|
64
|
+
# That's why we have to unindent the original snippet
|
65
|
+
# provided by the +ActiveRecord::Dumper##table+ method call
|
66
|
+
# back by 2 space characters.
|
67
|
+
#
|
68
|
+
# The `.strip << "\n"` is added for the compatibility
|
69
|
+
# with the +RubyBuilder+ which returns snippets
|
70
|
+
# having one trailing newline only.
|
71
|
+
def unindent(snippet)
|
72
|
+
snippet.lines.map { |line| line.sub(/^ {1,2}/, "") }.join.strip << "\n"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
|
3
|
+
module PGTrunk::Operations::Triggers
|
4
|
+
# @abstract
|
5
|
+
# @private
|
6
|
+
# Base class for operations with triggers
|
7
|
+
class Base < PGTrunk::Operation
|
8
|
+
# All attributes that can be used by trigger-related commands
|
9
|
+
attribute :columns, :pg_trunk_array_of_strings, default: []
|
10
|
+
attribute :constraint, :boolean
|
11
|
+
attribute :events, :pg_trunk_array_of_symbols, default: []
|
12
|
+
attribute :for_each, :pg_trunk_symbol
|
13
|
+
attribute :function, :pg_trunk_qualified_name
|
14
|
+
attribute :initially, :pg_trunk_symbol
|
15
|
+
attribute :replace_existing, :boolean
|
16
|
+
attribute :table, :pg_trunk_qualified_name
|
17
|
+
attribute :type, :pg_trunk_symbol
|
18
|
+
attribute :when, :string
|
19
|
+
|
20
|
+
# Generate missed name of the trigger
|
21
|
+
after_initialize { self.name = generated_name if name.blank? }
|
22
|
+
after_initialize { self.type ||= :after if constraint }
|
23
|
+
after_initialize { self.for_each ||= :row if constraint }
|
24
|
+
|
25
|
+
# Ensure correctness of present values
|
26
|
+
validates :table, presence: true
|
27
|
+
validates :for_each, inclusion: { in: %i[row statement] }, allow_nil: true
|
28
|
+
validates :initially, inclusion: { in: %i[immediate deferred] }, allow_nil: true
|
29
|
+
validates :type, inclusion: { in: %i[after before instead_of] }, allow_nil: true
|
30
|
+
validates :events,
|
31
|
+
inclusion: { in: %i[insert update delete truncate] },
|
32
|
+
allow_blank: true
|
33
|
+
validate do
|
34
|
+
next if name.blank?
|
35
|
+
|
36
|
+
errors.add :name, "can't have a schema" unless name.current_schema?
|
37
|
+
end
|
38
|
+
validate do
|
39
|
+
next unless initially && !constraint
|
40
|
+
|
41
|
+
errors.add :initially, "can be used for constraints only"
|
42
|
+
end
|
43
|
+
validate do
|
44
|
+
next unless columns.present? && type == :instead_of
|
45
|
+
|
46
|
+
errors.add :columns,
|
47
|
+
"can be defined for before/after update triggers only"
|
48
|
+
end
|
49
|
+
validate do
|
50
|
+
next unless constraint && type != :after && for_each != :row
|
51
|
+
|
52
|
+
errors.add :base, "Only AFTER EACH ROW triggers can be constraints"
|
53
|
+
end
|
54
|
+
validate do
|
55
|
+
next unless self.when && type == :instead_of
|
56
|
+
|
57
|
+
errors.add :when, "is not supported for INSTEAD OF triggers"
|
58
|
+
end
|
59
|
+
validate do
|
60
|
+
next if new_name.blank? || new_name.current_schema?
|
61
|
+
|
62
|
+
errors.add :base, "New name can't specify the schema"
|
63
|
+
end
|
64
|
+
|
65
|
+
# triggers are ordered by table and name
|
66
|
+
def <=>(other)
|
67
|
+
return unless other.is_a?(self.class)
|
68
|
+
|
69
|
+
result = table <=> other.table
|
70
|
+
result.zero? ? super : result
|
71
|
+
end
|
72
|
+
|
73
|
+
# Support `table` and `name` in positional arguments.
|
74
|
+
# @example
|
75
|
+
# add_trigger :users, :my_trigger, **opts
|
76
|
+
ruby_params :table, :name
|
77
|
+
|
78
|
+
ruby_snippet do |s|
|
79
|
+
s.ruby_param(table.lean) if table.present?
|
80
|
+
s.ruby_param(name.lean) if custom_name?
|
81
|
+
s.ruby_param(to: new_name.lean) if custom_name?(new_name)
|
82
|
+
s.ruby_param(if_exists: true) if if_exists
|
83
|
+
s.ruby_param(:replace_existing, true) if replace_existing
|
84
|
+
|
85
|
+
s.ruby_line(:function, function.lean) if function.present?
|
86
|
+
s.ruby_line(:when, self.when)
|
87
|
+
s.ruby_line(:constraint, true) if constraint
|
88
|
+
s.ruby_line(:for_each, for_each) if for_each&.== :row
|
89
|
+
s.ruby_line(:type, type) if type.present?
|
90
|
+
s.ruby_line(:events, events) if events.present?
|
91
|
+
s.ruby_line(:columns, columns) if columns.present?
|
92
|
+
s.ruby_line(:initially, initially)
|
93
|
+
s.ruby_line(:comment, comment, from: from_comment)
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
# Generate the name of the trigger using the essential options
|
99
|
+
# @return [PGTrunk::QualifiedName]
|
100
|
+
def generated_name
|
101
|
+
return @generated_name if instance_variable_defined?(:@generated_name)
|
102
|
+
|
103
|
+
@generated_name = begin
|
104
|
+
return if [table, function, type, events].any?(&:blank?)
|
105
|
+
|
106
|
+
key_options = to_h.reject { |_, v| v.blank? }.slice(
|
107
|
+
:table, :function, :for_each, :type, :events,
|
108
|
+
)
|
109
|
+
identifier = "#{table.lean}_#{key_options}_tg"
|
110
|
+
hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10)
|
111
|
+
PGTrunk::QualifiedName.wrap("tg_rails_#{hashed_identifier}")
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def custom_name?(qname = name)
|
116
|
+
qname&.differs_from?(/^tg_rails_\w{10}$/)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
|
3
|
+
# @!method ActiveRecord::Migration#create_trigger(table, name = nil, **options, &block)
|
4
|
+
# Create a trigger for a table
|
5
|
+
#
|
6
|
+
# @param [#to_s] table (nil) The qualified name of the table
|
7
|
+
# @param [#to_s] name (nil) The name of the trigger
|
8
|
+
# @option [Boolean] :if_exists (false) Suppress the error when the trigger is absent
|
9
|
+
# @yield [Proc] the block with the trigger's definition
|
10
|
+
# @yieldparam The receiver of methods specifying the trigger
|
11
|
+
#
|
12
|
+
# The trigger can be changed using `CREATE OR REPLACE TRIGGER` command:
|
13
|
+
#
|
14
|
+
# change_trigger "users", "do_something" do |t|
|
15
|
+
# t.function "do_something()", from: "do_something_different()"
|
16
|
+
# t.for_each :row # from: :statement
|
17
|
+
# t.type :after, from: :before
|
18
|
+
# t.events %i[insert update], from: %i[insert]
|
19
|
+
# t.comment "Does something useful", from: ""
|
20
|
+
# end
|
21
|
+
|
22
|
+
module PGTrunk::Operations::Triggers
|
23
|
+
# @private
|
24
|
+
class ChangeTrigger < Base
|
25
|
+
validates :replace_existing, :new_name, :version, absence: true
|
26
|
+
validate { errors.add :base, "Changes can't be blank" if changes.blank? }
|
27
|
+
validate do
|
28
|
+
next if if_exists
|
29
|
+
|
30
|
+
errors.add :base, "The trigger cannot be found" unless create_trigger
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_sql(server_version)
|
34
|
+
return create_trigger&.to_sql(server_version) if server_version >= "14"
|
35
|
+
|
36
|
+
raise "The operation is supported by PostgreSQL server v14+"
|
37
|
+
end
|
38
|
+
|
39
|
+
def invert
|
40
|
+
irreversible!("if_exists: true") if if_exists
|
41
|
+
undefined = inversion.select { |_, v| v.nil? }.keys.join(", ").presence
|
42
|
+
raise IrreversibleMigration.new(self, nil, <<~MSG.squish) if undefined
|
43
|
+
Undefined values to revert #{undefined}.
|
44
|
+
MSG
|
45
|
+
|
46
|
+
self.class.new(**inversion, table: table, name: name)
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def changes
|
52
|
+
@changes ||= {
|
53
|
+
type: type.presence,
|
54
|
+
events: events.presence,
|
55
|
+
columns: columns.presence,
|
56
|
+
constraint: constraint,
|
57
|
+
for_each: for_each,
|
58
|
+
function: function.presence,
|
59
|
+
initially: initially,
|
60
|
+
when: self.when.presence,
|
61
|
+
comment: comment,
|
62
|
+
}.compact
|
63
|
+
end
|
64
|
+
|
65
|
+
def inversion
|
66
|
+
changes
|
67
|
+
.each_with_object({}) { |(k, _), obj| obj[k] = send(:"from_#{k}") }
|
68
|
+
.tap do |i|
|
69
|
+
i[:for_each] ||= (%i[statement row] - [for_each]).first if for_each
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def create_trigger
|
74
|
+
return if name.blank? || table.blank?
|
75
|
+
|
76
|
+
@create_trigger ||=
|
77
|
+
CreateTrigger
|
78
|
+
.find { |o| o.name == name && o.table == table }
|
79
|
+
&.tap { |o| o.attributes = { **changes, replace_existing: true } }
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|