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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +4 -15
- data/CHANGELOG.md +21 -0
- data/README.md +3 -1
- data/lib/pg_trunk/core/operation/attributes.rb +1 -1
- data/lib/pg_trunk/core/railtie/custom_types.rb +5 -6
- data/lib/pg_trunk/operations/check_constraints/add_check_constraint.rb +42 -33
- data/lib/pg_trunk/operations/check_constraints/drop_check_constraint.rb +51 -40
- data/lib/pg_trunk/operations/check_constraints/rename_check_constraint.rb +39 -30
- data/lib/pg_trunk/operations/check_constraints/validate_check_constraint.rb +28 -21
- data/lib/pg_trunk/operations/composite_types/change_composite_type.rb +59 -50
- data/lib/pg_trunk/operations/composite_types/create_composite_type.rb +23 -19
- data/lib/pg_trunk/operations/composite_types/drop_composite_type.rb +48 -43
- data/lib/pg_trunk/operations/composite_types/rename_composite_type.rb +15 -12
- data/lib/pg_trunk/operations/domains/change_domain.rb +53 -47
- data/lib/pg_trunk/operations/domains/create_domain.rb +28 -25
- data/lib/pg_trunk/operations/domains/drop_domain.rb +50 -41
- data/lib/pg_trunk/operations/domains/rename_domain.rb +17 -12
- data/lib/pg_trunk/operations/enums/change_enum.rb +37 -32
- data/lib/pg_trunk/operations/enums/create_enum.rb +23 -20
- data/lib/pg_trunk/operations/enums/drop_enum.rb +50 -39
- data/lib/pg_trunk/operations/enums/rename_enum.rb +17 -12
- data/lib/pg_trunk/operations/foreign_keys/add_foreign_key.rb +58 -49
- data/lib/pg_trunk/operations/foreign_keys/drop_foreign_key.rb +57 -48
- data/lib/pg_trunk/operations/foreign_keys/rename_foreign_key.rb +38 -29
- data/lib/pg_trunk/operations/functions/change_function.rb +53 -47
- data/lib/pg_trunk/operations/functions/create_function.rb +75 -64
- data/lib/pg_trunk/operations/functions/drop_function.rb +78 -65
- data/lib/pg_trunk/operations/functions/rename_function.rb +29 -22
- data/lib/pg_trunk/operations/materialized_views/change_materialized_view.rb +65 -55
- data/lib/pg_trunk/operations/materialized_views/create_materialized_view.rb +82 -71
- data/lib/pg_trunk/operations/materialized_views/drop_materialized_view.rb +59 -46
- data/lib/pg_trunk/operations/materialized_views/refresh_materialized_view.rb +29 -24
- data/lib/pg_trunk/operations/materialized_views/rename_materialized_view.rb +29 -22
- data/lib/pg_trunk/operations/procedures/change_procedure.rb +53 -46
- data/lib/pg_trunk/operations/procedures/create_procedure.rb +63 -52
- data/lib/pg_trunk/operations/procedures/drop_procedure.rb +56 -45
- data/lib/pg_trunk/operations/procedures/rename_procedure.rb +29 -22
- data/lib/pg_trunk/operations/rules/base.rb +77 -0
- data/lib/pg_trunk/operations/rules/create_rule.rb +155 -0
- data/lib/pg_trunk/operations/rules/drop_rule.rb +94 -0
- data/lib/pg_trunk/operations/rules/rename_rule.rb +62 -0
- data/lib/pg_trunk/operations/rules.rb +13 -0
- data/lib/pg_trunk/operations/sequences/base.rb +79 -0
- data/lib/pg_trunk/operations/sequences/change_sequence.rb +142 -0
- data/lib/pg_trunk/operations/sequences/create_sequence.rb +180 -0
- data/lib/pg_trunk/operations/sequences/drop_sequence.rb +82 -0
- data/lib/pg_trunk/operations/sequences/rename_sequence.rb +64 -0
- data/lib/pg_trunk/operations/sequences.rb +14 -0
- data/lib/pg_trunk/operations/statistics/create_statistics.rb +67 -56
- data/lib/pg_trunk/operations/statistics/drop_statistics.rb +64 -53
- data/lib/pg_trunk/operations/statistics/rename_statistics.rb +18 -13
- data/lib/pg_trunk/operations/triggers/change_trigger.rb +23 -18
- data/lib/pg_trunk/operations/triggers/create_trigger.rb +63 -54
- data/lib/pg_trunk/operations/triggers/drop_trigger.rb +55 -46
- data/lib/pg_trunk/operations/triggers/rename_trigger.rb +51 -48
- data/lib/pg_trunk/operations/views/change_view.rb +47 -38
- data/lib/pg_trunk/operations/views/create_view.rb +56 -45
- data/lib/pg_trunk/operations/views/drop_view.rb +59 -46
- data/lib/pg_trunk/operations/views/rename_view.rb +27 -20
- data/lib/pg_trunk/operations.rb +2 -0
- data/lib/pg_trunk/version.rb +1 -1
- data/pg_trunk.gemspec +0 -1
- data/spec/operations/rules/create_rule_spec.rb +119 -0
- data/spec/operations/rules/drop_rule_spec.rb +117 -0
- data/spec/operations/rules/rename_rule_spec.rb +148 -0
- data/spec/operations/sequences/change_sequence_spec.rb +134 -0
- data/spec/operations/sequences/create_sequence_spec.rb +156 -0
- data/spec/operations/sequences/drop_sequence_spec.rb +102 -0
- data/spec/operations/sequences/rename_sequence_spec.rb +100 -0
- metadata +22 -68
@@ -1,50 +1,61 @@
|
|
1
1
|
# frozen_string_literal: false
|
2
2
|
|
3
|
-
# @!
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
# @option [
|
10
|
-
# @option [#to_s] :
|
11
|
-
# @option [
|
12
|
-
#
|
13
|
-
#
|
14
|
-
# @
|
15
|
-
# @
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
24
|
-
#
|
25
|
-
#
|
26
|
-
#
|
27
|
-
#
|
28
|
-
#
|
29
|
-
#
|
30
|
-
#
|
31
|
-
#
|
32
|
-
#
|
33
|
-
#
|
34
|
-
#
|
35
|
-
#
|
36
|
-
#
|
37
|
-
#
|
38
|
-
#
|
39
|
-
#
|
40
|
-
#
|
41
|
-
#
|
42
|
-
#
|
43
|
-
#
|
44
|
-
#
|
45
|
-
#
|
46
|
-
#
|
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
|
-
# @!
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
7
|
-
# @
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
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
|