pg_trunk 0.1.0 → 0.2.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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +4 -15
  3. data/CHANGELOG.md +21 -0
  4. data/README.md +3 -1
  5. data/lib/pg_trunk/core/operation/attributes.rb +1 -1
  6. data/lib/pg_trunk/core/railtie/custom_types.rb +5 -6
  7. data/lib/pg_trunk/operations/check_constraints/add_check_constraint.rb +42 -33
  8. data/lib/pg_trunk/operations/check_constraints/drop_check_constraint.rb +51 -40
  9. data/lib/pg_trunk/operations/check_constraints/rename_check_constraint.rb +39 -30
  10. data/lib/pg_trunk/operations/check_constraints/validate_check_constraint.rb +28 -21
  11. data/lib/pg_trunk/operations/composite_types/change_composite_type.rb +59 -50
  12. data/lib/pg_trunk/operations/composite_types/create_composite_type.rb +23 -19
  13. data/lib/pg_trunk/operations/composite_types/drop_composite_type.rb +48 -43
  14. data/lib/pg_trunk/operations/composite_types/rename_composite_type.rb +15 -12
  15. data/lib/pg_trunk/operations/domains/change_domain.rb +53 -47
  16. data/lib/pg_trunk/operations/domains/create_domain.rb +28 -25
  17. data/lib/pg_trunk/operations/domains/drop_domain.rb +50 -41
  18. data/lib/pg_trunk/operations/domains/rename_domain.rb +17 -12
  19. data/lib/pg_trunk/operations/enums/change_enum.rb +37 -32
  20. data/lib/pg_trunk/operations/enums/create_enum.rb +23 -20
  21. data/lib/pg_trunk/operations/enums/drop_enum.rb +50 -39
  22. data/lib/pg_trunk/operations/enums/rename_enum.rb +17 -12
  23. data/lib/pg_trunk/operations/foreign_keys/add_foreign_key.rb +58 -49
  24. data/lib/pg_trunk/operations/foreign_keys/drop_foreign_key.rb +57 -48
  25. data/lib/pg_trunk/operations/foreign_keys/rename_foreign_key.rb +38 -29
  26. data/lib/pg_trunk/operations/functions/change_function.rb +53 -47
  27. data/lib/pg_trunk/operations/functions/create_function.rb +75 -64
  28. data/lib/pg_trunk/operations/functions/drop_function.rb +78 -65
  29. data/lib/pg_trunk/operations/functions/rename_function.rb +29 -22
  30. data/lib/pg_trunk/operations/materialized_views/change_materialized_view.rb +65 -55
  31. data/lib/pg_trunk/operations/materialized_views/create_materialized_view.rb +82 -71
  32. data/lib/pg_trunk/operations/materialized_views/drop_materialized_view.rb +59 -46
  33. data/lib/pg_trunk/operations/materialized_views/refresh_materialized_view.rb +29 -24
  34. data/lib/pg_trunk/operations/materialized_views/rename_materialized_view.rb +29 -22
  35. data/lib/pg_trunk/operations/procedures/change_procedure.rb +53 -46
  36. data/lib/pg_trunk/operations/procedures/create_procedure.rb +63 -52
  37. data/lib/pg_trunk/operations/procedures/drop_procedure.rb +56 -45
  38. data/lib/pg_trunk/operations/procedures/rename_procedure.rb +29 -22
  39. data/lib/pg_trunk/operations/rules/base.rb +77 -0
  40. data/lib/pg_trunk/operations/rules/create_rule.rb +155 -0
  41. data/lib/pg_trunk/operations/rules/drop_rule.rb +94 -0
  42. data/lib/pg_trunk/operations/rules/rename_rule.rb +62 -0
  43. data/lib/pg_trunk/operations/rules.rb +13 -0
  44. data/lib/pg_trunk/operations/sequences/base.rb +79 -0
  45. data/lib/pg_trunk/operations/sequences/change_sequence.rb +142 -0
  46. data/lib/pg_trunk/operations/sequences/create_sequence.rb +180 -0
  47. data/lib/pg_trunk/operations/sequences/drop_sequence.rb +82 -0
  48. data/lib/pg_trunk/operations/sequences/rename_sequence.rb +64 -0
  49. data/lib/pg_trunk/operations/sequences.rb +14 -0
  50. data/lib/pg_trunk/operations/statistics/create_statistics.rb +67 -56
  51. data/lib/pg_trunk/operations/statistics/drop_statistics.rb +64 -53
  52. data/lib/pg_trunk/operations/statistics/rename_statistics.rb +18 -13
  53. data/lib/pg_trunk/operations/triggers/change_trigger.rb +23 -18
  54. data/lib/pg_trunk/operations/triggers/create_trigger.rb +63 -54
  55. data/lib/pg_trunk/operations/triggers/drop_trigger.rb +55 -46
  56. data/lib/pg_trunk/operations/triggers/rename_trigger.rb +51 -48
  57. data/lib/pg_trunk/operations/views/change_view.rb +47 -38
  58. data/lib/pg_trunk/operations/views/create_view.rb +56 -45
  59. data/lib/pg_trunk/operations/views/drop_view.rb +59 -46
  60. data/lib/pg_trunk/operations/views/rename_view.rb +27 -20
  61. data/lib/pg_trunk/operations.rb +2 -0
  62. data/lib/pg_trunk/version.rb +1 -1
  63. data/pg_trunk.gemspec +0 -1
  64. data/spec/operations/rules/create_rule_spec.rb +119 -0
  65. data/spec/operations/rules/drop_rule_spec.rb +117 -0
  66. data/spec/operations/rules/rename_rule_spec.rb +148 -0
  67. data/spec/operations/sequences/change_sequence_spec.rb +134 -0
  68. data/spec/operations/sequences/create_sequence_spec.rb +156 -0
  69. data/spec/operations/sequences/drop_sequence_spec.rb +102 -0
  70. data/spec/operations/sequences/rename_sequence_spec.rb +100 -0
  71. metadata +22 -68
@@ -1,50 +1,61 @@
1
1
  # frozen_string_literal: false
2
2
 
3
- # @!method ActiveRecord::Migration#drop_procedure(name, **options, &block)
4
- # Drop a procedure
5
- #
6
- # @param [#to_s] name (nil)
7
- # The qualified name of the procedure with arguments and returned value type
8
- # @option [Boolean] :if_exists (false) Suppress the error when the procedure is absent
9
- # @option [#to_s] :language ("sql") The language (like "sql" or "plpgsql")
10
- # @option [#to_s] :body (nil) The body of the procedure
11
- # @option [Symbol] :security (:invoker) Define the role under which the procedure is invoked
12
- # Supported values: :invoker (default), :definer
13
- # @option [#to_s] :comment The description of the procedure
14
- # @yield [Proc] the block with the procedure's definition
15
- # @yieldparam The receiver of methods specifying the procedure
16
- #
17
- # A procedure can be dropped by a plain name:
18
- #
19
- # drop_procedure "set_foo"
20
- #
21
- # If several overloaded procedures have the name,
22
- # then you must specify the signature having
23
- # types of attributes at least:
24
- #
25
- # drop_procedure "set_foo(int)"
26
- #
27
- # In both cases above the operation is irreversible. To make it
28
- # inverted you have to provide a full signature along with
29
- # the body definition. The other options are supported as well:
30
- #
31
- # drop_procedure "metadata.set_foo(a int)" do |p|
32
- # p.language "sql" # (default)
33
- # p.body <<~SQL
34
- # SET foo = a
35
- # SQL
36
- # p.security :invoker # (default), also :definer
37
- # p.comment "Multiplies 2 integers"
38
- # SQL
39
- #
40
- # The operation can be called with `if_exists` option. In this case
41
- # it would do nothing when no procedure existed.
42
- #
43
- # drop_procedure "metadata.set_foo(a int)", if_exists: true
44
- #
45
- # Notice, that this option make the operation irreversible because of
46
- # uncertainty about the previous state of the database.
47
-
3
+ # @!parse
4
+ # class ActiveRecord::Migration
5
+ # # Drop a procedure
6
+ # #
7
+ # # @param [#to_s] name (nil)
8
+ # # The qualified name of the procedure with arguments and returned value type
9
+ # # @option options [Boolean] :if_exists (false) Suppress the error when the procedure is absent
10
+ # # @option options [#to_s] :language ("sql") The language (like "sql" or "plpgsql")
11
+ # # @option options [#to_s] :body (nil) The body of the procedure
12
+ # # @option options [Symbol] :security (:invoker) Define the role under which the procedure is invoked
13
+ # # Supported values: :invoker (default), :definer
14
+ # # @option options [#to_s] :comment The description of the procedure
15
+ # # @yield [p] the block with the procedure's definition
16
+ # # @yieldparam Object receiver of methods specifying the procedure
17
+ # # @return [void]
18
+ # #
19
+ # # A procedure can be dropped by a plain name:
20
+ # #
21
+ # # ```ruby
22
+ # # drop_procedure "set_foo"
23
+ # # ```
24
+ # #
25
+ # # If several overloaded procedures have the name,
26
+ # # then you must specify the signature having
27
+ # # types of attributes at least:
28
+ # #
29
+ # # ```ruby
30
+ # # drop_procedure "set_foo(int)"
31
+ # # ```
32
+ # #
33
+ # # In both cases above the operation is irreversible. To make it
34
+ # # inverted you have to provide a full signature along with
35
+ # # the body definition. The other options are supported as well:
36
+ # #
37
+ # # ```ruby
38
+ # # drop_procedure "metadata.set_foo(a int)" do |p|
39
+ # # p.language "sql" # (default)
40
+ # # p.body <<~SQL
41
+ # # SET foo = a
42
+ # # SQL
43
+ # # p.security :invoker # (default), also :definer
44
+ # # p.comment "Multiplies 2 integers"
45
+ # # SQL
46
+ # # ```
47
+ # #
48
+ # # The operation can be called with `if_exists` option. In this case
49
+ # # it would do nothing when no procedure existed.
50
+ # #
51
+ # # ```ruby
52
+ # # drop_procedure "metadata.set_foo(a int)", if_exists: true
53
+ # # ```
54
+ # #
55
+ # # Notice, that this option make the operation irreversible because of
56
+ # # uncertainty about the previous state of the database.
57
+ # def drop_procedure(name, **options, &block); end
58
+ # end
48
59
  module PGTrunk::Operations::Procedures
49
60
  # @private
50
61
  class DropProcedure < Base
@@ -1,27 +1,34 @@
1
1
  # frozen_string_literal: false
2
2
 
3
- # @!method ActiveRecord::Migration#rename_procedure(name, to:)
4
- # Change the name and/or schema of a procedure
5
- #
6
- # @param [#to_s] :name (nil) The qualified name of the procedure
7
- # @option [#to_s] :to (nil) The new qualified name for the procedure
8
- #
9
- # A procedure can be renamed by changing both the name
10
- # and the schema (namespace) it belongs to.
11
- #
12
- # If there are no overloaded procedures, then you can use a plain name:
13
- #
14
- # rename_procedure "math.set_foo", to: "public.foo_setup"
15
- #
16
- # otherwise the types of attributes must be explicitly specified.
17
- #
18
- # rename_procedure "math.set_foo(int)", to: "public.foo_setup"
19
- #
20
- # Any specification of attributes in `to:` option
21
- # is ignored because they cannot be changed anyway.
22
- #
23
- # The operation is always reversible.
24
-
3
+ # @!parse
4
+ # class ActiveRecord::Migration
5
+ # # Change the name and/or schema of a procedure
6
+ # #
7
+ # # @param [#to_s] :name (nil) The qualified name of the procedure
8
+ # # @option options [#to_s] :to (nil) The new qualified name for the procedure
9
+ # # @return [void]
10
+ # #
11
+ # # A procedure can be renamed by changing both the name
12
+ # # and the schema (namespace) it belongs to.
13
+ # #
14
+ # # If there are no overloaded procedures, then you can use a plain name:
15
+ # #
16
+ # # ```ruby
17
+ # # rename_procedure "math.set_foo", to: "public.foo_setup"
18
+ # # ```
19
+ # #
20
+ # # otherwise the types of attributes must be explicitly specified.
21
+ # #
22
+ # # ```ruby
23
+ # # rename_procedure "math.set_foo(int)", to: "public.foo_setup"
24
+ # # ```
25
+ # #
26
+ # # Any specification of attributes in `to:` option
27
+ # # is ignored because they cannot be changed anyway.
28
+ # #
29
+ # # The operation is always reversible.
30
+ # def rename_procedure(name, to:); end
31
+ # end
25
32
  module PGTrunk::Operations::Procedures
26
33
  # @private
27
34
  class RenameProcedure < Base
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: false
2
+
3
+ module PGTrunk::Operations::Rules
4
+ # @abstract
5
+ # @private
6
+ # Base class for operations with rules
7
+ class Base < PGTrunk::Operation
8
+ attribute :command, :string
9
+ attribute :event, :pg_trunk_symbol
10
+ attribute :kind, :pg_trunk_symbol
11
+ attribute :replace_existing, :boolean
12
+ attribute :table, :pg_trunk_qualified_name
13
+ attribute :where, :string
14
+
15
+ # Generate missed name from table & expression
16
+ after_initialize { self.name = generated_name if name.blank? }
17
+
18
+ # Ensure correctness of present values
19
+ # The table must be defined because the name only
20
+ # is not enough to identify the constraint.
21
+ validates :if_not_exists, absence: true
22
+ validates :table, :name, presence: true
23
+ validates :kind, inclusion: { in: %i[instead also] }, allow_nil: true
24
+ validates :event, inclusion: { in: %i[insert update delete] }, allow_nil: true
25
+
26
+ # By default rules are sorted by tables and names.
27
+ def <=>(other)
28
+ return unless other.is_a?(self.class)
29
+
30
+ result = table <=> other.table
31
+ result.zero? ? super : result
32
+ end
33
+
34
+ # Support `table` and `name` in positional arguments
35
+ # @example
36
+ # create_rule :users, "_do_nothing"
37
+ ruby_params :table, :name
38
+
39
+ # Snippet to be used in all operations with rules
40
+ ruby_snippet do |s|
41
+ s.ruby_param(table.lean) if table.present?
42
+ s.ruby_param(name.name) if custom_name?
43
+ s.ruby_param(if_exists: true) if if_exists
44
+ s.ruby_param(replace_existing: true) if replace_existing
45
+ s.ruby_param(force: :cascade) if force == :cascade
46
+
47
+ s.ruby_line(:event, event) if event.present?
48
+ s.ruby_line(:kind, :instead) if kind == :instead
49
+ s.ruby_line(:where, where) if where.present?
50
+ s.ruby_line(:command, command) if command.present?
51
+ s.ruby_line(:comment, comment) if comment.present?
52
+ end
53
+
54
+ private
55
+
56
+ # *************************************************************************
57
+ # Helpers for operation definitions
58
+ # *************************************************************************
59
+
60
+ def generated_name
61
+ return @generated_name if instance_variable_defined?(:@generated_name)
62
+
63
+ @generated_name = begin
64
+ return if table.blank? || event.blank?
65
+
66
+ key_options = { event: event, kind: (kind || :also) }
67
+ identifier = "#{table.lean}_#{key_options}_rule"
68
+ hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10)
69
+ PGTrunk::QualifiedName.wrap("rule_rails_#{hashed_identifier}")
70
+ end
71
+ end
72
+
73
+ def custom_name?(qname = name)
74
+ qname&.differs_from?(/^rule_rails_\w+$/)
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: false
2
+
3
+ # @!parse
4
+ # class ActiveRecord::Migration
5
+ # # Create a rule
6
+ # #
7
+ # # @param [#to_s] table (nil) The qualified name of the table
8
+ # # @param [#to_s] name (nil) The name of the rule (unique within the table)
9
+ # # @option options [Boolean] :replace_existing (false) If the rule should overwrite an existing one
10
+ # # @option options [Symbol] :event (nil) The type of the query the rule is applied to.
11
+ # # Supported values: :update, :insert, :delete
12
+ # # @option options [Symbol] :kind (:also) The kind of the rule (either :also or :instead).
13
+ # # In case of `instead` the original query wouldn't be executed, only the `command` is.
14
+ # # @option options [String] :where (nil) The condition (SQL) for the rule to be applied.
15
+ # # @option options [String] :command (nil) The SQL command to be added by the rule.
16
+ # # @yield [r] the block with the rule's definition
17
+ # # @yieldparam Object receiver of methods specifying the rule
18
+ # # @return [void]
19
+ # #
20
+ # # @notice `SELECT` rules are not supported by the gem.
21
+ # #
22
+ # # To create a rule you must define table, and event (operation) for the rule.
23
+ # # Usually you also supposed to define a command, but in case the `kind` is set
24
+ # # to `:instead`, missing the command would provide `INSTEAD DO NOTHING` rule.
25
+ # #
26
+ # # ```ruby
27
+ # # create_rule "users" do |r|
28
+ # # r.event :insert
29
+ # # r.kind :instead
30
+ # # r.comment "Forbid insertion to the table"
31
+ # # SQL
32
+ # # ```
33
+ # #
34
+ # # By default the kind is set to `:also`, in this case the `command` is needed as well:
35
+ # #
36
+ # # ```ruby
37
+ # # create_rule "users", "_count_insertion" do |r|
38
+ # # r.event :insert
39
+ # # r.command <<~SQL
40
+ # # UPDATE counters SET user_inserts = user_inserts + 1
41
+ # # SQL
42
+ # # r.comment "Count insertion to the table"
43
+ # # SQL
44
+ # # ```
45
+ # #
46
+ # # With a `when` option you can also specify a condition:
47
+ # #
48
+ # # ```ruby
49
+ # # create_rule "users", "_forbid_grants" do |r|
50
+ # # r.event :update
51
+ # # r.kind :instead
52
+ # # r.where "NOT old.admin AND new.admin"
53
+ # # r.comment "Forbid granting admin rights"
54
+ # # SQL
55
+ # # ```
56
+ # #
57
+ # # With a `replace_existing: true` option,
58
+ # # the rule will be created using the `CREATE OR REPLACE` clause.
59
+ # # In this case the migration is irreversible because we
60
+ # # don't know if and how to restore the previous definition.
61
+ # #
62
+ # # ```ruby
63
+ # # create_rule "users", "_forbid_insertion", replace_existing: true do |r|
64
+ # # r.event :insert
65
+ # # r.kind :instead
66
+ # # r.comment "Forbid insertion to the table"
67
+ # # SQL
68
+ # # ```
69
+ # def create_rule(table, name = nil, **options, &block); end
70
+ # end
71
+ module PGTrunk::Operations::Rules
72
+ # @private
73
+ class CreateRule < Base
74
+ validates :if_exists, :force, :new_name, absence: true
75
+ validates :event, presence: true
76
+ validate do
77
+ errors.add :command, :blank if kind != :instead && command.blank?
78
+ end
79
+
80
+ from_sql do |_server_version|
81
+ <<~SQL
82
+ SELECT
83
+ r.oid,
84
+ (c.relnamespace::regnamespace || '.' || c.relname) AS table,
85
+ r.rulename AS name,
86
+ (
87
+ CASE
88
+ WHEN r.ev_type = '1' THEN 'select'
89
+ WHEN r.ev_type = '2' THEN 'update'
90
+ WHEN r.ev_type = '3' THEN 'insert'
91
+ WHEN r.ev_type = '4' THEN 'delete'
92
+ END
93
+ ) AS event,
94
+ ( CASE WHEN r.is_instead THEN 'instead' ELSE 'also' END ) AS kind,
95
+ pg_get_expr(r.ev_qual, r.oid, true) AS "where",
96
+ regexp_replace(
97
+ regexp_replace(
98
+ pg_get_ruledef(r.oid, true),
99
+ '.+ DO +(INSTEAD +)?(ALSO +)?(NOTHING *)?| *;',
100
+ '',
101
+ 'g'
102
+ ), '(\n|\s)+', ' ', 'g'
103
+ ) AS command,
104
+ d.description AS comment
105
+ FROM pg_rewrite r
106
+ JOIN pg_class c ON c.oid = r.ev_class
107
+ JOIN pg_trunk t ON t.oid = r.oid
108
+ AND t.classid = 'pg_rewrite'::regclass
109
+ LEFT JOIN pg_description d ON d.objoid = r.oid
110
+ AND d.classoid = 'pg_rewrite'::regclass
111
+ SQL
112
+ end
113
+
114
+ def to_sql(_server_version)
115
+ [create_rule, *comment_rule, register_rule].join(" ")
116
+ end
117
+
118
+ def invert
119
+ irreversible!("replace_existing: true") if replace_existing
120
+ DropRule.new(**to_h)
121
+ end
122
+
123
+ private
124
+
125
+ def create_rule
126
+ sql = "CREATE"
127
+ sql << " OR REPLACE" if replace_existing
128
+ sql << " RULE #{name.to_sql} AS ON #{event.to_s.upcase}"
129
+ sql << " TO #{table.to_sql}"
130
+ sql << " WHERE #{where}" if where.present?
131
+ sql << " DO #{kind == :instead ? 'INSTEAD' : 'ALSO'}"
132
+ sql << " #{command.presence || 'NOTHING'}"
133
+ sql << ";"
134
+ end
135
+
136
+ def comment_rule
137
+ <<~SQL.squish if comment.present?
138
+ COMMENT ON RULE #{name.to_sql} ON #{table.to_sql}#{' '}
139
+ IS $comment$#{comment}$comment$;
140
+ SQL
141
+ end
142
+
143
+ def register_rule
144
+ <<~SQL.squish
145
+ INSERT INTO pg_trunk (oid, classid)
146
+ SELECT r.oid, 'pg_rewrite'::regclass
147
+ FROM pg_rewrite r JOIN pg_class c ON c.oid = r.ev_class
148
+ WHERE r.rulename = #{name.quoted}
149
+ AND c.relname = '#{table.name}'
150
+ AND c.relnamespace = #{table.namespace}
151
+ ON CONFLICT DO NOTHING;
152
+ SQL
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: false
2
+
3
+ # @!parse
4
+ # class ActiveRecord::Migration
5
+ # # Drop a rule
6
+ # #
7
+ # # @param [#to_s] table (nil) The qualified name of the table
8
+ # # @param [#to_s] name (nil) The name of the rule (unique within the table)
9
+ # # @option options [Boolean] :if_exists (false) Suppress the error when the rule is absent.
10
+ # # @option options [Symbol] :force (:restrict) Define how to process dependent objects
11
+ # # Supported values: :restrict (default), :cascade (for cascade deletion)
12
+ # # @option options [Symbol] :event (nil) The type of the query the rule is applied to.
13
+ # # Supported values: :update, :insert, :delete
14
+ # # @option options [Symbol] :kind (:also) The kind of the rule (either :also or :instead).
15
+ # # In case of `instead` the original query wouldn't be executed, only the `command` is.
16
+ # # @option options [String] :where (nil) The condition (SQL) for the rule to be applied.
17
+ # # @option options [String] :command (nil) The SQL command to be added by the rule.
18
+ # # @yield [r] the block with the rule's definition
19
+ # # @yieldparam Object receiver of methods specifying the rule
20
+ # # @return [void]
21
+ # #
22
+ # # The rule can be identified by the table and explicit name
23
+ # #
24
+ # # ```ruby
25
+ # # drop_rule :users, "_forbid_insertion"
26
+ # # ```
27
+ # #
28
+ # # Alternatively the name can be got from kind and event.
29
+ # #
30
+ # # ```ruby
31
+ # # drop_rule :users do |r|
32
+ # # r.event :insert
33
+ # # r.kind :instead
34
+ # # r.comment "Forbid insertion to the table"
35
+ # # end
36
+ # # ```
37
+ # #
38
+ # # To made operation reversible all the necessary parameters must be provided
39
+ # # like in the `create_rule` operation:
40
+ # #
41
+ # # ```ruby
42
+ # # drop_rule "users", "_count_insertion" do |r|
43
+ # # r.event :insert
44
+ # # r.command <<~SQL
45
+ # # UPDATE counters SET user_inserts = user_inserts + 1
46
+ # # SQL
47
+ # # r.comment "Count insertion to the table"
48
+ # # SQL
49
+ # # ```
50
+ # #
51
+ # # The operation can be called with `if_exists` option.
52
+ # #
53
+ # # ```ruby
54
+ # # drop_rule :users, if_exists: true do |r|
55
+ # # # event and kind here are used to define a name
56
+ # # r.event :insert
57
+ # # r.kind :instead
58
+ # # end
59
+ # # ```
60
+ # #
61
+ # # With the `force: :cascade` option the operation would remove
62
+ # # all the objects that use the rule.
63
+ # #
64
+ # # ```ruby
65
+ # # drop_rule :users, force: :cascade do |r|
66
+ # # r.event :insert
67
+ # # r.kind :instead
68
+ # # end
69
+ # # ```
70
+ # #
71
+ # # In both cases the operation becomes irreversible due to
72
+ # # uncertainty of the previous state of the database.
73
+ # def drop_rule(table, name = nil, **options, &block); end
74
+ # end
75
+ module PGTrunk::Operations::Rules
76
+ # @private
77
+ class DropRule < Base
78
+ validates :replace_existing, :new_name, absence: true
79
+
80
+ def to_sql(_version)
81
+ sql = "DROP RULE"
82
+ sql << " IF EXISTS" if if_exists
83
+ sql << " #{name.name.inspect} ON #{table.to_sql}"
84
+ sql << " CASCADE" if force == :cascade
85
+ sql << ";"
86
+ end
87
+
88
+ def invert
89
+ irreversible!("if_exists: true") if if_exists
90
+ irreversible!("force: :cascade") if force == :cascade
91
+ CreateRule.new(**to_h.except(:force))
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: false
2
+
3
+ # @!parse
4
+ # class ActiveRecord::Migration
5
+ # # Rename a rule
6
+ # #
7
+ # # @param [#to_s] table (nil) The qualified name of the table
8
+ # # @param [#to_s] name (nil) The current name of the rule
9
+ # # @option options [#to_s] :to (nil) The new name for the rule
10
+ # # @yield [c] the block with the constraint's definition
11
+ # # @yieldparam Object receiver of methods specifying the constraint
12
+ # # @return [void]
13
+ # #
14
+ # # A rule can be identified by the table and explicit name
15
+ # #
16
+ # # ```ruby
17
+ # # rename_rule :users, "_forbid_insertion", to: "_skip_insertion"
18
+ # # ```
19
+ # #
20
+ # # Alternatively the name can be got from the event and kind.
21
+ # #
22
+ # # ```ruby
23
+ # # rename_rule :users, to: "_skip_insertion" do |r|
24
+ # # r.event :insert
25
+ # # r.kind :instead
26
+ # # end
27
+ # # ```
28
+ # #
29
+ # # The name can be reset to auto-generated when
30
+ # # the `:to` option is missed or blank:
31
+ # #
32
+ # # ```ruby
33
+ # # rename_rule :users, "_skip_insertion" do |r|
34
+ # # r.event :insert
35
+ # # r.kind :instead
36
+ # # end
37
+ # # ```
38
+ # #
39
+ # # The operation is always reversible.
40
+ # def rename_rule(table, name = nil, **options, &block); end
41
+ # end
42
+ module PGTrunk::Operations::Rules
43
+ # @private
44
+ class RenameRule < Base
45
+ after_initialize { self.new_name = generated_name if new_name.blank? }
46
+
47
+ validates :new_name, presence: true
48
+ validates :where, :command, :replace_existing, :force, :if_exists,
49
+ absence: true
50
+
51
+ def to_sql(_version)
52
+ <<~SQL
53
+ ALTER RULE #{name.to_sql} ON #{table.to_sql}
54
+ RENAME TO #{new_name.to_sql};
55
+ SQL
56
+ end
57
+
58
+ def invert
59
+ self.class.new(**to_h, name: new_name, to: name)
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ # nodoc
4
+ module PGTrunk::Operations
5
+ # @private
6
+ # Namespace for operations with rules
7
+ module Rules
8
+ require_relative "rules/base"
9
+ require_relative "rules/create_rule"
10
+ require_relative "rules/drop_rule"
11
+ require_relative "rules/rename_rule"
12
+ end
13
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: false
2
+
3
+ module PGTrunk::Operations::Sequences
4
+ # @abstract
5
+ # @private
6
+ # Base class for operations with sequences
7
+ class Base < PGTrunk::Operation
8
+ attribute :type, :string, aliases: :as
9
+ attribute :increment_by, :integer
10
+ attribute :min_value, :integer
11
+ attribute :max_value, :integer
12
+ attribute :start_with, :integer
13
+ attribute :cache, :integer
14
+ attribute :cycle, :boolean
15
+ attribute :table, :string
16
+ attribute :column, :string
17
+
18
+ def owned_by(table, column)
19
+ self.table = table
20
+ self.column = column
21
+ end
22
+
23
+ # Ensure correctness of present values
24
+ # The table must be defined because the name only
25
+ # is not enough to identify the constraint.
26
+ validates :name, presence: true
27
+ validates :cache, numericality: { greater_than_or_equal_to: 1 }, allow_nil: true
28
+ validate { errors.add :base, "Increment must not be zero" if increment_by&.zero? }
29
+ validate do
30
+ next unless table.present? ^ column.present?
31
+
32
+ errors.add :base, "Both table and column must be set"
33
+ end
34
+ validate do
35
+ next if min_value.blank? || max_value.blank? || min_value <= max_value
36
+
37
+ errors.add :base, "Min value must not exceed max value"
38
+ end
39
+ validate do
40
+ next if start_with.blank? || min_value.blank? || start_with >= min_value
41
+
42
+ errors.add :base, "start value cannot be less than min value"
43
+ end
44
+ validate do
45
+ next if start_with.blank? || max_value.blank? || start_with <= max_value
46
+
47
+ errors.add :base, "start value cannot be greater than max value"
48
+ end
49
+
50
+ # Use comparison by name from pg_trunk operations base class (default)
51
+ # Support name as the only positional argument (default)
52
+
53
+ # Snippet to be used in all operations with rules
54
+ ruby_snippet do |s|
55
+ s.ruby_param(name.lean) if name.present?
56
+ s.ruby_param(as: type) if type.present? && from_type.blank?
57
+ s.ruby_param(to: new_name) if new_name.present?
58
+ s.ruby_param(if_exists: true) if if_exists
59
+ s.ruby_param(if_not_exists: true) if if_not_exists
60
+ s.ruby_param(force: :cascade) if force == :cascade
61
+
62
+ s.ruby_line(:type, type, from: from_type) if from_type.present?
63
+ s.ruby_line(:owned_by, table, column) if table.present? || column.present?
64
+ s.ruby_line(:increment_by, increment_by, from: from_increment_by) if increment_by&.!= 1
65
+ s.ruby_line(:min_value, min_value, from: from_min_value) if min_value.present?
66
+ s.ruby_line(:max_value, max_value, from: from_max_value) if max_value.present?
67
+ s.ruby_line(:start_with, start_with, from: from_start_with) if custom_start?
68
+ s.ruby_line(:cache, cache, from: from_cache) if cache&.!= 1
69
+ s.ruby_line(:cycle, cycle) unless cycle.nil?
70
+ s.ruby_line(:comment, comment, from: from_comment) if comment.present?
71
+ end
72
+
73
+ private
74
+
75
+ def custom_start?
76
+ increment_by&.<(0) ? start_with&.!=(max_value) : start_with&.!=(min_value)
77
+ end
78
+ end
79
+ end