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.
Files changed (196) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +87 -0
  3. data/.gitignore +9 -0
  4. data/.rspec +4 -0
  5. data/.rubocop.yml +92 -0
  6. data/.yardopts +4 -0
  7. data/CHANGELOG.md +31 -0
  8. data/CONTRIBUTING.md +17 -0
  9. data/Gemfile +22 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +141 -0
  12. data/Rakefile +16 -0
  13. data/bin/console +8 -0
  14. data/bin/rake +19 -0
  15. data/bin/rspec +19 -0
  16. data/bin/setup +8 -0
  17. data/bin/yard +19 -0
  18. data/lib/pg_trunk/core/adapters/postgres.rb +80 -0
  19. data/lib/pg_trunk/core/dependencies_resolver.rb +101 -0
  20. data/lib/pg_trunk/core/generators.rb +140 -0
  21. data/lib/pg_trunk/core/operation/attributes.rb +78 -0
  22. data/lib/pg_trunk/core/operation/callbacks.rb +40 -0
  23. data/lib/pg_trunk/core/operation/generators.rb +51 -0
  24. data/lib/pg_trunk/core/operation/inversion.rb +70 -0
  25. data/lib/pg_trunk/core/operation/registration.rb +55 -0
  26. data/lib/pg_trunk/core/operation/ruby_builder.rb +112 -0
  27. data/lib/pg_trunk/core/operation/ruby_helpers.rb +99 -0
  28. data/lib/pg_trunk/core/operation/sql_helpers.rb +44 -0
  29. data/lib/pg_trunk/core/operation/validations.rb +21 -0
  30. data/lib/pg_trunk/core/operation.rb +78 -0
  31. data/lib/pg_trunk/core/qualified_name.rb +165 -0
  32. data/lib/pg_trunk/core/railtie/command_recorder.rb +30 -0
  33. data/lib/pg_trunk/core/railtie/custom_types.rb +37 -0
  34. data/lib/pg_trunk/core/railtie/migration.rb +50 -0
  35. data/lib/pg_trunk/core/railtie/migrator.rb +22 -0
  36. data/lib/pg_trunk/core/railtie/schema_dumper.rb +75 -0
  37. data/lib/pg_trunk/core/railtie/schema_migration.rb +22 -0
  38. data/lib/pg_trunk/core/railtie/statements.rb +21 -0
  39. data/lib/pg_trunk/core/railtie.rb +35 -0
  40. data/lib/pg_trunk/core/registry.rb +159 -0
  41. data/lib/pg_trunk/core/serializers/array_of_hashes_serializer.rb +28 -0
  42. data/lib/pg_trunk/core/serializers/array_of_strings_serializer.rb +29 -0
  43. data/lib/pg_trunk/core/serializers/array_of_symbols_serializer.rb +28 -0
  44. data/lib/pg_trunk/core/serializers/array_serializer.rb +22 -0
  45. data/lib/pg_trunk/core/serializers/lowercase_string_serializer.rb +21 -0
  46. data/lib/pg_trunk/core/serializers/multiline_text_serializer.rb +21 -0
  47. data/lib/pg_trunk/core/serializers/qualified_name_serializer.rb +27 -0
  48. data/lib/pg_trunk/core/serializers/symbol_serializer.rb +22 -0
  49. data/lib/pg_trunk/core/serializers.rb +16 -0
  50. data/lib/pg_trunk/core/validators/all_items_valid_validator.rb +15 -0
  51. data/lib/pg_trunk/core/validators/difference_validator.rb +19 -0
  52. data/lib/pg_trunk/core/validators.rb +10 -0
  53. data/lib/pg_trunk/core.rb +21 -0
  54. data/lib/pg_trunk/generators.rb +7 -0
  55. data/lib/pg_trunk/operations/check_constraints/add_check_constraint.rb +109 -0
  56. data/lib/pg_trunk/operations/check_constraints/base.rb +69 -0
  57. data/lib/pg_trunk/operations/check_constraints/drop_check_constraint.rb +60 -0
  58. data/lib/pg_trunk/operations/check_constraints/rename_check_constraint.rb +54 -0
  59. data/lib/pg_trunk/operations/check_constraints/validate_check_constraint.rb +39 -0
  60. data/lib/pg_trunk/operations/check_constraints.rb +14 -0
  61. data/lib/pg_trunk/operations/composite_types/base.rb +61 -0
  62. data/lib/pg_trunk/operations/composite_types/change_composite_type.rb +136 -0
  63. data/lib/pg_trunk/operations/composite_types/column.rb +118 -0
  64. data/lib/pg_trunk/operations/composite_types/create_composite_type.rb +99 -0
  65. data/lib/pg_trunk/operations/composite_types/drop_composite_type.rb +67 -0
  66. data/lib/pg_trunk/operations/composite_types/rename_composite_type.rb +44 -0
  67. data/lib/pg_trunk/operations/composite_types.rb +15 -0
  68. data/lib/pg_trunk/operations/domains/base.rb +46 -0
  69. data/lib/pg_trunk/operations/domains/change_domain.rb +140 -0
  70. data/lib/pg_trunk/operations/domains/constraint.rb +93 -0
  71. data/lib/pg_trunk/operations/domains/create_domain.rb +124 -0
  72. data/lib/pg_trunk/operations/domains/drop_domain.rb +65 -0
  73. data/lib/pg_trunk/operations/domains/rename_domain.rb +44 -0
  74. data/lib/pg_trunk/operations/domains.rb +15 -0
  75. data/lib/pg_trunk/operations/enums/base.rb +47 -0
  76. data/lib/pg_trunk/operations/enums/change.rb +55 -0
  77. data/lib/pg_trunk/operations/enums/change_enum.rb +119 -0
  78. data/lib/pg_trunk/operations/enums/create_enum.rb +83 -0
  79. data/lib/pg_trunk/operations/enums/drop_enum.rb +63 -0
  80. data/lib/pg_trunk/operations/enums/rename_enum.rb +44 -0
  81. data/lib/pg_trunk/operations/enums.rb +15 -0
  82. data/lib/pg_trunk/operations/foreign_keys/add_foreign_key.rb +174 -0
  83. data/lib/pg_trunk/operations/foreign_keys/base.rb +155 -0
  84. data/lib/pg_trunk/operations/foreign_keys/drop_foreign_key.rb +76 -0
  85. data/lib/pg_trunk/operations/foreign_keys/rename_foreign_key.rb +63 -0
  86. data/lib/pg_trunk/operations/foreign_keys.rb +16 -0
  87. data/lib/pg_trunk/operations/functions/base.rb +54 -0
  88. data/lib/pg_trunk/operations/functions/change_function.rb +108 -0
  89. data/lib/pg_trunk/operations/functions/create_function.rb +198 -0
  90. data/lib/pg_trunk/operations/functions/drop_function.rb +88 -0
  91. data/lib/pg_trunk/operations/functions/rename_function.rb +57 -0
  92. data/lib/pg_trunk/operations/functions.rb +14 -0
  93. data/lib/pg_trunk/operations/indexes/add_index.rb +68 -0
  94. data/lib/pg_trunk/operations/indexes.rb +10 -0
  95. data/lib/pg_trunk/operations/materialized_views/base.rb +79 -0
  96. data/lib/pg_trunk/operations/materialized_views/change_materialized_view.rb +139 -0
  97. data/lib/pg_trunk/operations/materialized_views/column.rb +94 -0
  98. data/lib/pg_trunk/operations/materialized_views/create_materialized_view.rb +170 -0
  99. data/lib/pg_trunk/operations/materialized_views/drop_materialized_view.rb +70 -0
  100. data/lib/pg_trunk/operations/materialized_views/refresh_materialized_view.rb +48 -0
  101. data/lib/pg_trunk/operations/materialized_views/rename_materialized_view.rb +61 -0
  102. data/lib/pg_trunk/operations/materialized_views.rb +17 -0
  103. data/lib/pg_trunk/operations/procedures/base.rb +42 -0
  104. data/lib/pg_trunk/operations/procedures/change_procedure.rb +107 -0
  105. data/lib/pg_trunk/operations/procedures/create_procedure.rb +146 -0
  106. data/lib/pg_trunk/operations/procedures/drop_procedure.rb +66 -0
  107. data/lib/pg_trunk/operations/procedures/rename_procedure.rb +57 -0
  108. data/lib/pg_trunk/operations/procedures.rb +14 -0
  109. data/lib/pg_trunk/operations/statistics/base.rb +94 -0
  110. data/lib/pg_trunk/operations/statistics/create_statistics.rb +181 -0
  111. data/lib/pg_trunk/operations/statistics/drop_statistics.rb +75 -0
  112. data/lib/pg_trunk/operations/statistics/rename_statistics.rb +48 -0
  113. data/lib/pg_trunk/operations/statistics.rb +13 -0
  114. data/lib/pg_trunk/operations/tables/create_table.rb +75 -0
  115. data/lib/pg_trunk/operations/tables.rb +10 -0
  116. data/lib/pg_trunk/operations/triggers/base.rb +119 -0
  117. data/lib/pg_trunk/operations/triggers/change_trigger.rb +82 -0
  118. data/lib/pg_trunk/operations/triggers/create_trigger.rb +208 -0
  119. data/lib/pg_trunk/operations/triggers/drop_trigger.rb +66 -0
  120. data/lib/pg_trunk/operations/triggers/rename_trigger.rb +71 -0
  121. data/lib/pg_trunk/operations/triggers.rb +14 -0
  122. data/lib/pg_trunk/operations/views/base.rb +38 -0
  123. data/lib/pg_trunk/operations/views/change_view.rb +90 -0
  124. data/lib/pg_trunk/operations/views/create_view.rb +115 -0
  125. data/lib/pg_trunk/operations/views/drop_view.rb +69 -0
  126. data/lib/pg_trunk/operations/views/rename_view.rb +58 -0
  127. data/lib/pg_trunk/operations/views.rb +14 -0
  128. data/lib/pg_trunk/operations.rb +23 -0
  129. data/lib/pg_trunk/version.rb +6 -0
  130. data/lib/pg_trunk.rb +27 -0
  131. data/pg_trunk.gemspec +34 -0
  132. data/spec/dummy/.gitignore +16 -0
  133. data/spec/dummy/Rakefile +15 -0
  134. data/spec/dummy/bin/bundle +6 -0
  135. data/spec/dummy/bin/rails +6 -0
  136. data/spec/dummy/bin/rake +6 -0
  137. data/spec/dummy/config/application.rb +18 -0
  138. data/spec/dummy/config/boot.rb +7 -0
  139. data/spec/dummy/config/database.yml +14 -0
  140. data/spec/dummy/config/environment.rb +7 -0
  141. data/spec/dummy/config.ru +6 -0
  142. data/spec/dummy/db/materialized_views/admin_users_v01.sql +1 -0
  143. data/spec/dummy/db/migrate/.keep +0 -0
  144. data/spec/dummy/db/schema.rb +18 -0
  145. data/spec/dummy/db/views/admin_users_v01.sql +1 -0
  146. data/spec/dummy/db/views/admin_users_v02.sql +1 -0
  147. data/spec/operations/check_constraints/add_check_constraint_spec.rb +85 -0
  148. data/spec/operations/check_constraints/drop_check_constraint_spec.rb +111 -0
  149. data/spec/operations/check_constraints/rename_check_constraint_spec.rb +90 -0
  150. data/spec/operations/composite_types/change_composite_type_spec.rb +257 -0
  151. data/spec/operations/composite_types/create_composite_type_spec.rb +55 -0
  152. data/spec/operations/composite_types/drop_composite_type_spec.rb +109 -0
  153. data/spec/operations/composite_types/rename_composite_type_spec.rb +74 -0
  154. data/spec/operations/dependency_resolver_spec.rb +177 -0
  155. data/spec/operations/domains/change_domain_spec.rb +287 -0
  156. data/spec/operations/domains/create_domain_spec.rb +69 -0
  157. data/spec/operations/domains/drop_domain_spec.rb +119 -0
  158. data/spec/operations/domains/rename_domain_spec.rb +70 -0
  159. data/spec/operations/enums/change_enum_spec.rb +157 -0
  160. data/spec/operations/enums/create_enum_spec.rb +40 -0
  161. data/spec/operations/enums/drop_enum_spec.rb +120 -0
  162. data/spec/operations/enums/rename_enum_spec.rb +72 -0
  163. data/spec/operations/foreign_keys/add_foreign_key_spec.rb +208 -0
  164. data/spec/operations/foreign_keys/drop_foreign_key_spec.rb +167 -0
  165. data/spec/operations/foreign_keys/rename_foreign_key_spec.rb +101 -0
  166. data/spec/operations/functions/change_function_spec.rb +166 -0
  167. data/spec/operations/functions/create_function_spec.rb +192 -0
  168. data/spec/operations/functions/drop_function_spec.rb +182 -0
  169. data/spec/operations/functions/rename_function_spec.rb +101 -0
  170. data/spec/operations/indexes/add_index_spec.rb +94 -0
  171. data/spec/operations/materialized_views/change_materialized_view_spec.rb +190 -0
  172. data/spec/operations/materialized_views/create_materialized_view_spec.rb +144 -0
  173. data/spec/operations/materialized_views/drop_materialized_view_spec.rb +145 -0
  174. data/spec/operations/materialized_views/refresh_materialized_view_spec.rb +79 -0
  175. data/spec/operations/materialized_views/rename_materialized_view_spec.rb +88 -0
  176. data/spec/operations/procedures/change_procedure_spec.rb +175 -0
  177. data/spec/operations/procedures/create_procedure_spec.rb +151 -0
  178. data/spec/operations/procedures/drop_procedure_spec.rb +159 -0
  179. data/spec/operations/procedures/rename_procedure_spec.rb +107 -0
  180. data/spec/operations/statistics/create_statistics_spec.rb +230 -0
  181. data/spec/operations/statistics/drop_statistics_spec.rb +106 -0
  182. data/spec/operations/statistics/rename_statistics_spec.rb +129 -0
  183. data/spec/operations/tables/create_table_spec.rb +53 -0
  184. data/spec/operations/tables/rename_table_spec.rb +37 -0
  185. data/spec/operations/triggers/change_trigger_spec.rb +195 -0
  186. data/spec/operations/triggers/create_trigger_spec.rb +104 -0
  187. data/spec/operations/triggers/drop_trigger_spec.rb +124 -0
  188. data/spec/operations/triggers/rename_trigger_spec.rb +160 -0
  189. data/spec/operations/views/change_view_spec.rb +144 -0
  190. data/spec/operations/views/create_view_spec.rb +134 -0
  191. data/spec/operations/views/drop_view_spec.rb +146 -0
  192. data/spec/operations/views/rename_view_spec.rb +85 -0
  193. data/spec/pg_trunk/dependencies_resolver_spec.rb +43 -0
  194. data/spec/spec_helper.rb +28 -0
  195. data/spec/support/migrations_helper.rb +376 -0
  196. 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,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ # nodoc
4
+ module PGTrunk::Operations
5
+ # @private
6
+ # Namespace for operations with tables
7
+ module Tables
8
+ require_relative "tables/create_table"
9
+ end
10
+ 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