pg_trunk 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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