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