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