pg_trunk 0.1.3 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d88867c31da4259d601293ec91a915c070cef21a29ebe82e97a18a7dd0c72931
4
- data.tar.gz: c18bc215374043cf53bfdeae0245d537899dba92c9d665adda0dd7f05eba2890
3
+ metadata.gz: c7e02c0f3a19350a502afda3e8697421912161b9bd800eb6302738cc623eb3eb
4
+ data.tar.gz: c0108a41cabe1fd1eebc0695e222ceafeb27c195b9c8fe2766e1f4ced4142c63
5
5
  SHA512:
6
- metadata.gz: 17b2ec446bf2e44a04d7a9bd2e09451730321de3c08b3ca9fdf4faeb5f10894d933721c58b617f4309a900df2abd50609c6ac2295254afad727d992de8107106
7
- data.tar.gz: 11de4394260f0b6b1012c577b78ac344fd72ae78bc34f4465634557b6310b0982c54e29151a9a673b2f94bd11e889889ec16e7a69acf9e6c4e796eab94364ef9
6
+ metadata.gz: 7af9d03c65275a700408841a032cf2f074349943a8d131e54d5dff4f467b9e4bb3fadce657d70469ec2cb1471b41578bef68fc91a92771429d3da84b648161f8
7
+ data.tar.gz: 9a47edbac23bffb1db4c74f0108f42c06c5e9d7aa28b5d213fed05f908a8e738114db1ad4c38557fde7db7e2d514cabf6646d65a8193c7e5bf664b4d20bc38ff
data/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ The noteworthy changes for each PGTrunk version are included here.
4
4
  The format is based on [Keep a Changelog] and this project adheres to [Semantic Versioning].
5
5
  For a complete changelog, see the [commits] for each version via the version links.
6
6
 
7
+ ## [0.2.0] (2022-01-26)
8
+
9
+ * Add support for sequences (nepalez)
10
+ * Fix inheritance of attribute aliases (nepalez)
11
+ * Fix documentation for rules (nepalez)
12
+
7
13
  ## [0.1.3] (2022-01-20)
8
14
 
9
15
  * Add support for rules (nepalez)
data/README.md CHANGED
@@ -73,6 +73,7 @@ As of today we support creation, modification and dropping the following objects
73
73
  - composite types
74
74
  - domains types
75
75
  - rules
76
+ - sequences
76
77
 
77
78
  For `tables` and `indexes` we reuse the ActiveRecord's native methods.
78
79
  For `check constraints` and `foreign keys` we support both the native definitions inside the table
@@ -51,7 +51,7 @@ class PGTrunk::Operation
51
51
  end
52
52
 
53
53
  def inherited(klass)
54
- klass.instance_variable_set(:@attr_aliases, attr_aliases)
54
+ klass.instance_variable_set(:@attr_aliases, attr_aliases.dup)
55
55
  super
56
56
  end
57
57
  end
@@ -23,7 +23,7 @@ module PGTrunk::Operations::Rules
23
23
  validates :kind, inclusion: { in: %i[instead also] }, allow_nil: true
24
24
  validates :event, inclusion: { in: %i[insert update delete] }, allow_nil: true
25
25
 
26
- # By default foreign keys are sorted by tables and names.
26
+ # By default rules are sorted by tables and names.
27
27
  def <=>(other)
28
28
  return unless other.is_a?(self.class)
29
29
 
@@ -14,7 +14,7 @@
14
14
  # # @option options [String] :where (nil) The condition (SQL) for the rule to be applied.
15
15
  # # @option options [String] :command (nil) The SQL command to be added by the rule.
16
16
  # # @yield [r] the block with the rule's definition
17
- # # @yieldparam Object receiver of methods specifying the procedure
17
+ # # @yieldparam Object receiver of methods specifying the rule
18
18
  # # @return [void]
19
19
  # #
20
20
  # # @notice `SELECT` rules are not supported by the gem.
@@ -16,7 +16,7 @@
16
16
  # # @option options [String] :where (nil) The condition (SQL) for the rule to be applied.
17
17
  # # @option options [String] :command (nil) The SQL command to be added by the rule.
18
18
  # # @yield [r] the block with the rule's definition
19
- # # @yieldparam Object receiver of methods specifying the procedure
19
+ # # @yieldparam Object receiver of methods specifying the rule
20
20
  # # @return [void]
21
21
  # #
22
22
  # # The rule can be identified by the table and explicit name
@@ -59,7 +59,7 @@
59
59
  # # ```
60
60
  # #
61
61
  # # With the `force: :cascade` option the operation would remove
62
- # # all the objects that use the type.
62
+ # # all the objects that use the rule.
63
63
  # #
64
64
  # # ```ruby
65
65
  # # drop_rule :users, force: :cascade do |r|
@@ -11,7 +11,7 @@
11
11
  # # @yieldparam Object receiver of methods specifying the constraint
12
12
  # # @return [void]
13
13
  # #
14
- # # A constraint can be identified by the table and explicit name
14
+ # # A rule can be identified by the table and explicit name
15
15
  # #
16
16
  # # ```ruby
17
17
  # # rename_rule :users, "_forbid_insertion", to: "_skip_insertion"
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # nodoc
4
- module PGTrunk::Rules
4
+ module PGTrunk::Operations
5
5
  # @private
6
6
  # Namespace for operations with rules
7
7
  module Rules
@@ -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
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: false
2
+
3
+ # @!parse
4
+ # class ActiveRecord::Migration
5
+ # # Modify a sequence
6
+ # #
7
+ # # @param [#to_s] name (nil) The qualified name of the sequence.
8
+ # # @option options [Boolean] :if_exists (false) Suppress the error when the sequence is absent.
9
+ # # @yield [s] the block with the sequence's definition.
10
+ # # @yieldparam Object receiver of methods specifying the sequence.
11
+ # # @return [void]
12
+ # #
13
+ # # The operation enables to alter a sequence without recreating it.
14
+ # # PostgreSQL allows any setting to be modified. The comment can be
15
+ # # changed as well.
16
+ # #
17
+ # # ```ruby
18
+ # # change_sequence "my_schema.global_id" do |s|
19
+ # # s.owned_by "", "", from: %w[users gid]
20
+ # # s.type "smallint", from: "integer"
21
+ # # s.iterate_by 1, from: 2
22
+ # # s.min_value 1, from: 0
23
+ # # s.max_value 2000, from: 1999
24
+ # # s.start_with 2, from: 1
25
+ # # s.cache 1, from: 10
26
+ # # s.cycle false
27
+ # # s.comment "Identifier", from: "Global identifier"
28
+ # # end
29
+ # # ```
30
+ # #
31
+ # # As in the snippet above, to make the change invertible,
32
+ # # you have to define from option for every changed attribute,
33
+ # # except for the boolean `cycle`.
34
+ # #
35
+ # # With the `if_exists: true` option, the operation won't raise
36
+ # # when the sequence is absent.
37
+ # #
38
+ # # ```ruby
39
+ # # change_sequence "my_schema.global_id", if_exists: true do |s|
40
+ # # s.type "smallint"
41
+ # # s.iterate_by 1
42
+ # # s.min_value 1
43
+ # # s.max_value 2000
44
+ # # s.start_with 2
45
+ # # s.cache 1
46
+ # # s.cycle false
47
+ # # s.comment "Identifier"
48
+ # # end
49
+ # # ```
50
+ # #
51
+ # # This option makes a migration irreversible due to uncertainty
52
+ # # of the previous state of the database. That's why in the last
53
+ # # example no `from:` option was added (they are useless).
54
+ # def change_sequence(name, **options, &block); end
55
+ # end
56
+ module PGTrunk::Operations::Sequences
57
+ # @private
58
+ class ChangeSequence < Base
59
+ # Operation-specific validations
60
+ validate { errors.add :base, "Changes can't be blank" if changes.blank? }
61
+ validates :force, :if_not_exists, :new_name, absence: true
62
+
63
+ def owned_by(table, column, from: nil)
64
+ self.table = table
65
+ self.column = column
66
+ self.from_table, self.from_column = Array(from)
67
+ end
68
+
69
+ def to_sql(_version)
70
+ [*alter_sequence, *update_comment].join(" ")
71
+ end
72
+
73
+ def invert
74
+ irreversible!("if_exists: true") if if_exists
75
+ undefined = inversion.select { |_, v| v.nil? }.keys.join(", ").presence
76
+ raise IrreversibleMigration.new(self, nil, <<~MSG.squish) if undefined
77
+ Undefined values to revert #{undefined}.
78
+ MSG
79
+
80
+ self.class.new(name: name, **inversion) if inversion.any?
81
+ end
82
+
83
+ private
84
+
85
+ INF = (2**63) - 1
86
+
87
+ def changes
88
+ @changes ||= attributes.symbolize_keys.except(:name, :if_exists).compact
89
+ end
90
+
91
+ def inversion
92
+ @inversion ||= changes.each_with_object({}) do |(key, val), obj|
93
+ obj[key] = send(:"from_#{key}")
94
+ obj[key] = !val if [true, false].include?(val)
95
+ end
96
+ end
97
+
98
+ def alter_sequence
99
+ return if changes.except(:comment).blank?
100
+
101
+ sql = "ALTER SEQUENCE"
102
+ sql << " IF EXISTS" if if_exists
103
+ sql << " #{name.to_sql}"
104
+ sql << " AS #{type}" if type.present?
105
+ sql << " INCREMENT BY #{increment_by}" if increment_by.present?
106
+ sql << " MINVALUE #{min_value}" if min_value&.>(-INF)
107
+ sql << " NO MINVALUE" if min_value&.<=(-INF)
108
+ sql << " MAXVALUE #{max_value}" if max_value&.<(INF)
109
+ sql << " NO MAXVALUE" if max_value&.>=(INF)
110
+ sql << " START WITH #{start_with}" if start_with.present?
111
+ sql << " CACHE #{cache}" if cache.present?
112
+ sql << " OWNED BY #{table}.#{column}" if table.present? && column.present?
113
+ sql << " OWNED BY NONE" if table == "" || column == ""
114
+ sql << " CYCLE" if cycle
115
+ sql << " NO CYCLE" if cycle == false
116
+ sql << ";"
117
+ end
118
+
119
+ def update_comment
120
+ return unless comment
121
+ return <<~SQL.squish unless if_exists
122
+ COMMENT ON SEQUENCE #{name.to_sql} IS $comment$#{comment}$comment$;
123
+ SQL
124
+
125
+ # change the comment conditionally
126
+ <<~SQL.squish
127
+ DO $$
128
+ BEGIN
129
+ IF EXISTS (
130
+ SELECT FROM pg_sequence s JOIN pg_class c ON c.oid = s.seqrelid
131
+ WHERE c.relname = #{name.quoted}
132
+ AND c.relnamespace = #{name.namespace}
133
+ ) THEN
134
+ COMMENT ON SEQUENCE #{name.to_sql}
135
+ IS $comment$#{comment}$comment$;
136
+ END IF;
137
+ END
138
+ $$;
139
+ SQL
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: false
2
+
3
+ # @!parse
4
+ # class ActiveRecord::Migration
5
+ # # Create a sequence
6
+ # #
7
+ # # @param [#to_s] name (nil) The qualified name of the sequence
8
+ # # @option options [#to_s] :as ("bigint") The type of the sequence's value
9
+ # # Supported values: "bigint" (or "int8", default), "integer" (or "int4"), "smallint" ("int2").
10
+ # # @option options [Boolean] :if_not_exists (false)
11
+ # # Suppress the error when the sequence already existed.
12
+ # # @option options [Integer] :increment_by (1) Non-zero step of the sequence (either positive or negative).
13
+ # # @option options [Integer] :min_value (nil) Minimum value of the sequence.
14
+ # # @option options [Integer] :max_value (nil) Maximum value of the sequence.
15
+ # # @option options [Integer] :start_with (nil) The first value of the sequence.
16
+ # # @option options [Integer] :cache (1) The number of values to be generated and cached.
17
+ # # @option options [Boolean] :cycle (false) If the sequence should be reset to start
18
+ # # after its value reaches min/max value.
19
+ # # @option options [#to_s] :comment (nil) The comment describing the sequence.
20
+ # # @yield [s] the block with the sequence's definition
21
+ # # @yieldparam Object receiver of methods specifying the sequence
22
+ # # @return [void]
23
+ # #
24
+ # # The sequence can be created by its qualified name only
25
+ # #
26
+ # # ```ruby
27
+ # # create_sequence "my_schema.global_id"
28
+ # # ```
29
+ # #
30
+ # # we also support all PostgreSQL settings for the sequence:
31
+ # #
32
+ # # ```ruby
33
+ # # create_sequence "my_schema.global_id", as: "integer" do |s|
34
+ # # s.iterate_by 2
35
+ # # s.min_value 0
36
+ # # s.max_value 1999
37
+ # # s.start_with 1
38
+ # # s.cache 10
39
+ # # s.cycle true
40
+ # # s.comment "Global identifier"
41
+ # # end
42
+ # # ```
43
+ # #
44
+ # # Using a block method `s.owned_by` you can bind the sequence to
45
+ # # some table's column. This means the sequence is dependent from
46
+ # # the column and will be dropped along with it. Notice that the
47
+ # # name of the table is NOT qualified because the table MUST belong
48
+ # # to the same schema as the sequence itself.
49
+ # #
50
+ # # ```ruby
51
+ # # create_table "users" do |t|
52
+ # # t.bigint :gid
53
+ # # end
54
+ # #
55
+ # # create_sequence "my_schema.global_id" do |s|
56
+ # # s.owned_by "users", "gid"
57
+ # # end
58
+ # # ```
59
+ # #
60
+ # # With the `if_not_exists: true` option the operation wouldn't raise
61
+ # # an exception in case the sequence has been already created.
62
+ # #
63
+ # # ```ruby
64
+ # # create_sequence "my_schema.global_id", if_not_exists: true
65
+ # # ```
66
+ # #
67
+ # # This option makes the migration irreversible due to uncertainty
68
+ # # of the previous state of the database.
69
+ # def create_sequence(name, **options, &block); end
70
+ # end
71
+ module PGTrunk::Operations::Sequences
72
+ # @private
73
+ class CreateSequence < Base
74
+ validates :if_exists, :force, :new_name, absence: true
75
+
76
+ from_sql do |_server_version|
77
+ <<~SQL
78
+ SELECT
79
+ c.oid,
80
+ (c.relnamespace::regnamespace || '.' || c.relname) AS name,
81
+ p.refobjid::regclass AS table,
82
+ a.attname AS column,
83
+ (
84
+ CASE WHEN s.seqtypid != 'int8'::regtype THEN format_type(s.seqtypid, 0) END
85
+ ) AS type,
86
+ ( CASE WHEN s.seqincrement != 1 THEN s.seqincrement END ) AS increment_by,
87
+ (
88
+ CASE
89
+ WHEN s.seqincrement > 0 THEN
90
+ CASE WHEN s.seqmin != 1 THEN s.seqmin END
91
+ ELSE
92
+ CASE
93
+ WHEN s.seqtypid = 'int2'::regtype AND s.seqmin = -32768 THEN NULL
94
+ WHEN s.seqtypid = 'int4'::regtype AND s.seqmin = -2147483648 THEN NULL
95
+ WHEN s.seqtypid = 'int8'::regtype AND s.seqmin = -9223372036854775808 THEN NULL
96
+ ELSE s.seqmin
97
+ END
98
+ END
99
+ ) AS min_value,
100
+ (
101
+ CASE
102
+ WHEN s.seqincrement < 0 THEN
103
+ CASE WHEN s.seqmax != -1 THEN s.seqmax END
104
+ ELSE
105
+ CASE
106
+ WHEN s.seqtypid = 'int2'::regtype AND s.seqmax = 32767 THEN NULL
107
+ WHEN s.seqtypid = 'int4'::regtype AND s.seqmax = 2147483647 THEN NULL
108
+ WHEN s.seqtypid = 'int8'::regtype AND s.seqmax = 9223372036854775807 THEN NULL
109
+ ELSE s.seqmax
110
+ END
111
+ END
112
+ ) AS max_value,
113
+ (
114
+ CASE
115
+ WHEN s.seqincrement > 0 AND s.seqstart = s.seqmin THEN NULL
116
+ WHEN s.seqincrement < 0 AND s.seqstart = s.seqmax THEN NULL
117
+ ELSE s.seqstart
118
+ END
119
+ ) AS start_with,
120
+ ( CASE WHEN s.seqcache != 1 THEN s.seqcache END ) AS cache,
121
+ ( CASE WHEN s.seqcycle THEN true END ) AS cycle,
122
+ d.description AS comment
123
+ FROM pg_sequence s
124
+ JOIN pg_class c ON c.oid = s.seqrelid
125
+ JOIN pg_trunk t ON t.oid = c.oid
126
+ AND t.classid = 'pg_sequence'::regclass
127
+ LEFT JOIN pg_depend p ON p.objid = c.oid
128
+ AND p.classid = 'pg_class'::regclass
129
+ AND p.refclassid = 'pg_class'::regclass
130
+ AND p.objsubid = 0 AND p.refobjsubid !=0
131
+ LEFT JOIN pg_attribute a ON a.attrelid = p.refobjid
132
+ AND a.attnum = p.refobjsubid
133
+ LEFT JOIN pg_description d ON d.objoid = c.oid;
134
+ SQL
135
+ end
136
+
137
+ def to_sql(_server_version)
138
+ [create_sequence, *comment_sequence, register_sequence].join(" ")
139
+ end
140
+
141
+ def invert
142
+ irreversible!("if_not_exists: true") if if_not_exists
143
+ DropSequence.new(**to_h.except(:if_not_exists))
144
+ end
145
+
146
+ private
147
+
148
+ def create_sequence
149
+ sql = "CREATE SEQUENCE"
150
+ sql << " IF NOT EXISTS" if if_not_exists
151
+ sql << " #{name.to_sql}"
152
+ sql << " AS #{type}" if type.present?
153
+ sql << " INCREMENT BY #{increment_by}" if increment_by.present?
154
+ sql << " MINVALUE #{min_value}" if min_value.present?
155
+ sql << " MAXVALUE #{max_value}" if max_value.present?
156
+ sql << " START WITH #{start_with}" if start_with.present?
157
+ sql << " CACHE #{cache}" if cache.present?
158
+ sql << " OWNED BY #{table}.#{column}" if table.present? && column.present?
159
+ sql << " CYCLE" if cycle
160
+ sql << ";"
161
+ end
162
+
163
+ def comment_sequence
164
+ <<~SQL.squish if comment.present?
165
+ COMMENT ON SEQUENCE #{name.to_sql} IS $comment$#{comment}$comment$;
166
+ SQL
167
+ end
168
+
169
+ def register_sequence
170
+ <<~SQL.squish
171
+ INSERT INTO pg_trunk (oid, classid)
172
+ SELECT c.oid, 'pg_sequence'::regclass
173
+ FROM pg_sequence s JOIN pg_class c ON c.oid = s.seqrelid
174
+ WHERE c.relname = #{name.quoted}
175
+ AND c.relnamespace = #{name.namespace}
176
+ ON CONFLICT DO NOTHING;
177
+ SQL
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: false
2
+
3
+ # @!parse
4
+ # class ActiveRecord::Migration
5
+ # # Drop a sequence
6
+ # #
7
+ # # @param [#to_s] name (nil) The qualified name of the sequence
8
+ # # @option options [#to_s] :as ("bigint") The type of the sequence's value
9
+ # # Supported values: "bigint" (or "int8", default), "integer" (or "int4"), "smallint" ("int2").
10
+ # # @option options [Boolean] :if_exists (false) Suppress the error when the sequence is absent.
11
+ # # @option options [Symbol] :force (:restrict) Define how to process dependent objects
12
+ # # Supported values: :restrict (default), :cascade.
13
+ # # @option options [Integer] :increment_by (1) Non-zero step of the sequence (either positive or negative).
14
+ # # @option options [Integer] :min_value (nil) Minimum value of the sequence.
15
+ # # @option options [Integer] :max_value (nil) Maximum value of the sequence.
16
+ # # @option options [Integer] :start_with (nil) The first value of the sequence.
17
+ # # @option options [Integer] :cache (1) The number of values to be generated and cached.
18
+ # # @option options [Boolean] :cycle (false) If the sequence should be reset to start
19
+ # # after its value reaches min/max value.
20
+ # # @option options [#to_s] :comment (nil) The comment describing the sequence.
21
+ # # @yield [s] the block with the sequence's definition
22
+ # # @yieldparam Object receiver of methods specifying the sequence
23
+ # # @return [void]
24
+ # #
25
+ # # The sequence can be dropped by its qualified name only
26
+ # #
27
+ # # ```ruby
28
+ # # drop_sequence "global_number"
29
+ # # ```
30
+ # #
31
+ # # For inversion provide options for the `create_sequence` operation as well:
32
+ # #
33
+ # # ```ruby
34
+ # # drop_sequence "global_id", as: "int2" do |s|
35
+ # # s.iterate_by 2
36
+ # # s.min_value 0
37
+ # # s.max_value 1999
38
+ # # s.start_with 1
39
+ # # s.cache 10
40
+ # # s.cycle true
41
+ # # s.comment "Global identifier"
42
+ # # end
43
+ # # ```
44
+ # #
45
+ # # The operation can be called with `if_exists` option to suppress
46
+ # # the exception in case when the sequence is absent:
47
+ # #
48
+ # # ```ruby
49
+ # # drop_sequence "global_number", if_exists: true
50
+ # # ```
51
+ # #
52
+ # # With the `force: :cascade` option the operation would remove
53
+ # # all the objects that use the sequence.
54
+ # #
55
+ # # ```ruby
56
+ # # drop_sequence "global_number", force: :cascade
57
+ # # ```
58
+ # #
59
+ # # In both cases the operation becomes irreversible due to
60
+ # # uncertainty of the previous state of the database.
61
+ # def drop_sequence(name, **options, &block); end
62
+ # end
63
+ module PGTrunk::Operations::Sequences
64
+ # @private
65
+ class DropSequence < Base
66
+ validates :if_not_exists, :new_name, absence: true
67
+
68
+ def to_sql(_version)
69
+ sql = "DROP SEQUENCE"
70
+ sql << " IF EXISTS" if if_exists
71
+ sql << " #{name.to_sql}"
72
+ sql << " CASCADE" if force == :cascade
73
+ sql << ";"
74
+ end
75
+
76
+ def invert
77
+ irreversible!("if_exists: true") if if_exists
78
+ irreversible!("force: :cascade") if force == :cascade
79
+ CreateSequence.new(**to_h.except(:if_exists, :force))
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: false
2
+
3
+ # @!parse
4
+ # class ActiveRecord::Migration
5
+ # # Rename a sequence
6
+ # #
7
+ # # @param [#to_s] name (nil) The current qualified name of the sequence
8
+ # # @option options [#to_s] :to (nil) The new qualified name for the sequence
9
+ # # @option options [Boolean] :if_exists (false) Suppress the error when the sequence is absent.
10
+ # # @return [void]
11
+ # #
12
+ # # The operation allows to change both name and schema
13
+ # #
14
+ # # ```ruby
15
+ # # rename_sequence "global_num", to: "sequences.global_number"
16
+ # # ```
17
+ # #
18
+ # # With the `if_exists: true` option the operation wouldn't raise
19
+ # # an exception in case the sequence hasn't been created yet.
20
+ # #
21
+ # # ```ruby
22
+ # # create_sequence "my_schema.global_id", if_exists: true
23
+ # # ```
24
+ # #
25
+ # # This option makes the migration irreversible due to uncertainty
26
+ # # of the previous state of the database.
27
+ # def rename_sequence(name, **options, &block); end
28
+ # end
29
+ module PGTrunk::Operations::Sequences
30
+ # @private
31
+ class RenameSequence < Base
32
+ validates :new_name, presence: true
33
+ validates :if_not_exists, :force, :type, :increment_by, :min_value,
34
+ :max_value, :start_with, :cache, :cycle, :comment, absence: true
35
+
36
+ def to_sql(_version)
37
+ [*change_schema, *change_name].join(" ")
38
+ end
39
+
40
+ def invert
41
+ irreversible!("if_exists: true") if if_exists
42
+ self.class.new(**to_h, name: new_name, to: name)
43
+ end
44
+
45
+ private
46
+
47
+ def change_schema
48
+ return if name.schema == new_name.schema
49
+
50
+ sql = "ALTER SEQUENCE"
51
+ sql << " IF EXISTS" if if_exists
52
+ sql << " #{name.to_sql} SET SCHEMA #{new_name.schema.inspect};"
53
+ end
54
+
55
+ def change_name
56
+ return if new_name.name == name.name
57
+
58
+ moved = name.merge(schema: new_name.schema)
59
+ sql = "ALTER SEQUENCE"
60
+ sql << " IF EXISTS" if if_exists
61
+ sql << " #{moved.to_sql} RENAME TO #{new_name.name.inspect};"
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # nodoc
4
+ module PGTrunk::Operations
5
+ # @private
6
+ # Namespace for operations with sequences
7
+ module Sequences
8
+ require_relative "sequences/base"
9
+ require_relative "sequences/change_sequence"
10
+ require_relative "sequences/create_sequence"
11
+ require_relative "sequences/drop_sequence"
12
+ require_relative "sequences/rename_sequence"
13
+ end
14
+ end
@@ -9,6 +9,7 @@ module PGTrunk
9
9
  require_relative "operations/enums"
10
10
  require_relative "operations/composite_types"
11
11
  require_relative "operations/domains"
12
+ require_relative "operations/sequences"
12
13
  require_relative "operations/tables"
13
14
  require_relative "operations/views"
14
15
  require_relative "operations/materialized_views"
@@ -2,5 +2,5 @@
2
2
 
3
3
  module PGTrunk
4
4
  # @private
5
- VERSION = "0.1.3"
5
+ VERSION = "0.2.0"
6
6
  end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe ActiveRecord::Migration, "#change_sequence" do
4
+ before { run_migration(old_snippet) }
5
+
6
+ let(:old_snippet) do
7
+ <<~RUBY
8
+ create_sequence "global_num", as: "integer" do |s|
9
+ s.increment_by 2
10
+ s.min_value 0
11
+ s.max_value 2000
12
+ s.start_with 1
13
+ s.cache 10
14
+ s.cycle true
15
+ s.comment "Sequence for global numbers (odds then evens)"
16
+ end
17
+ RUBY
18
+ end
19
+
20
+ context "with reversible changes" do
21
+ let(:migration) do
22
+ <<~RUBY
23
+ change_sequence "global_num" do |s|
24
+ s.type "bigint", from: "integer"
25
+ s.increment_by 3, from: 2
26
+ s.min_value 1, from: 0
27
+ s.max_value 3000, from: 2000
28
+ s.start_with 2, from: 1
29
+ s.cache 20, from: 10
30
+ s.cycle false
31
+ s.comment "Global numbers", from: "Sequence for global numbers (odds then evens)"
32
+ end
33
+ RUBY
34
+ end
35
+ let(:new_snippet) do
36
+ <<~RUBY
37
+ create_sequence "global_num" do |s|
38
+ s.increment_by 3
39
+ s.max_value 3000
40
+ s.start_with 2
41
+ s.cache 20
42
+ s.comment "Global numbers"
43
+ end
44
+ RUBY
45
+ end
46
+
47
+ its(:execution) { is_expected.to remove(old_snippet).from_schema }
48
+ its(:execution) { is_expected.to insert(new_snippet).into_schema }
49
+ its(:inversion) { is_expected.not_to change_schema }
50
+ end
51
+
52
+ context "with irreversible changes" do
53
+ let(:migration) do
54
+ <<~RUBY
55
+ change_sequence "global_num" do |s|
56
+ s.type "bigint"
57
+ s.increment_by 3
58
+ s.min_value 1
59
+ s.max_value 3000
60
+ s.start_with 2
61
+ s.cache 20
62
+ s.cycle false
63
+ s.comment "Global numbers"
64
+ end
65
+ RUBY
66
+ end
67
+ let(:new_snippet) do
68
+ <<~RUBY
69
+ create_sequence "global_num" do |s|
70
+ s.increment_by 3
71
+ s.max_value 3000
72
+ s.start_with 2
73
+ s.cache 20
74
+ s.comment "Global numbers"
75
+ end
76
+ RUBY
77
+ end
78
+
79
+ its(:execution) { is_expected.to remove(old_snippet).from_schema }
80
+ its(:execution) { is_expected.to insert(new_snippet).into_schema }
81
+ it { is_expected.to be_irreversible.because_of(/undefined values to revert/i) }
82
+ end
83
+
84
+ context "when sequence was absent" do
85
+ let(:old_snippet) { "" }
86
+
87
+ context "without the `:if_exists` option" do
88
+ let(:migration) do
89
+ <<~RUBY
90
+ change_sequence "global_num" do |s|
91
+ s.comment "Global numbers"
92
+ end
93
+ RUBY
94
+ end
95
+
96
+ its(:execution) { is_expected.to raise_error(StandardError) }
97
+ end
98
+
99
+ context "with the `if_exists: true` option" do
100
+ let(:migration) do
101
+ <<~RUBY
102
+ change_sequence "global_num", if_exists: true do |s|
103
+ s.comment "Global numbers"
104
+ end
105
+ RUBY
106
+ end
107
+
108
+ its(:execution) { is_expected.not_to change_schema }
109
+ it { is_expected.to be_irreversible.because_of(/if_exists: true/i) }
110
+ end
111
+ end
112
+
113
+ context "without changes" do
114
+ let(:migration) do
115
+ <<~RUBY
116
+ change_sequence "global_num"
117
+ RUBY
118
+ end
119
+
120
+ it { is_expected.to fail_validation.because(/changes can't be blank/i) }
121
+ end
122
+
123
+ context "without a name" do
124
+ let(:migration) do
125
+ <<~RUBY
126
+ change_sequence do |s|
127
+ s.comment "Global numbers"
128
+ end
129
+ RUBY
130
+ end
131
+
132
+ it { is_expected.to fail_validation.because(/name can't be blank/i) }
133
+ end
134
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe ActiveRecord::Migration, "#create_sequence" do
4
+ before_all do
5
+ run_migration <<~RUBY
6
+ create_schema :app
7
+
8
+ create_table :customers do |t|
9
+ t.bigint :global_num
10
+ end
11
+ RUBY
12
+ end
13
+
14
+ context "with a minimal definition" do
15
+ let(:migration) do
16
+ <<~RUBY
17
+ create_sequence "app.global_num"
18
+ RUBY
19
+ end
20
+
21
+ its(:execution) { is_expected.to insert(migration).into_schema }
22
+ its(:inversion) { is_expected.not_to change_schema }
23
+ end
24
+
25
+ context "with a table-agnostic definition" do
26
+ let(:migration) do
27
+ <<~RUBY
28
+ create_sequence "app.global_num", as: "integer" do |s|
29
+ s.increment_by 2
30
+ s.min_value 0
31
+ s.max_value 2000
32
+ s.start_with 1
33
+ s.cache 10
34
+ s.cycle true
35
+ s.comment "Sequence for global numbers (odds then evens)"
36
+ end
37
+ RUBY
38
+ end
39
+
40
+ its(:execution) { is_expected.to insert(migration).into_schema }
41
+ its(:inversion) { is_expected.not_to change_schema }
42
+ end
43
+
44
+ context "with a column-specific definition" do
45
+ let(:migration) do
46
+ <<~RUBY
47
+ create_sequence "global_num" do |s|
48
+ s.owned_by "customers", "global_num"
49
+ s.increment_by 2
50
+ s.min_value 0
51
+ s.max_value 2000
52
+ s.start_with 1
53
+ s.cache 10
54
+ s.cycle true
55
+ s.comment "Sequence for customers global_num"
56
+ end
57
+ RUBY
58
+ end
59
+
60
+ its(:execution) { is_expected.to insert(migration).into_schema }
61
+ its(:inversion) { is_expected.not_to change_schema }
62
+ end
63
+
64
+ context "when the sequence existed" do
65
+ before { run_migration(migration) }
66
+
67
+ context "without the `:if_not_exists` option" do
68
+ let(:migration) do
69
+ <<~RUBY
70
+ create_sequence "app.global_num"
71
+ RUBY
72
+ end
73
+
74
+ its(:execution) { is_expected.to raise_error(StandardError) }
75
+ end
76
+
77
+ context "with the `if_not_exists: true` option" do
78
+ let(:migration) do
79
+ <<~RUBY
80
+ create_sequence "app.global_num", if_not_exists: true
81
+ RUBY
82
+ end
83
+ let(:snippet) do
84
+ <<~RUBY
85
+ create_sequence "app.global_num"
86
+ RUBY
87
+ end
88
+
89
+ its(:execution) { is_expected.not_to change_schema }
90
+ it { is_expected.to be_irreversible.because_of(/if_not_exists: true/i) }
91
+ end
92
+ end
93
+
94
+ context "with a zero increment" do
95
+ let(:migration) do
96
+ <<~RUBY
97
+ create_sequence "app.global_number", increment_by: 0
98
+ RUBY
99
+ end
100
+
101
+ it { is_expected.to fail_validation.because(/increment must not be zero/i) }
102
+ end
103
+
104
+ context "with invalid min..max range" do
105
+ let(:migration) do
106
+ <<~RUBY
107
+ create_sequence "app.global_number", min_value: 2, max_value: 1
108
+ RUBY
109
+ end
110
+
111
+ it { is_expected.to fail_validation.because(/min value must not exceed max value/i) }
112
+ end
113
+
114
+ context "with start value out of min..max range" do
115
+ let(:migration) do
116
+ <<~RUBY
117
+ create_sequence "app.global_number",
118
+ min_value: 0,
119
+ max_value: 10,
120
+ start_with: -1
121
+ RUBY
122
+ end
123
+
124
+ it { is_expected.to fail_validation.because(/start value cannot be less than min value/i) }
125
+ end
126
+
127
+ context "with a zero cache" do
128
+ let(:migration) do
129
+ <<~RUBY
130
+ create_sequence "app.global_number", cache: 0
131
+ RUBY
132
+ end
133
+
134
+ it { is_expected.to fail_validation.because(/cache must be greater than or equal to 1/i) }
135
+ end
136
+
137
+ context "with a wrong type" do
138
+ let(:migration) do
139
+ <<~RUBY
140
+ create_sequence "app.global_number", as: "text"
141
+ RUBY
142
+ end
143
+
144
+ its(:execution) { is_expected.to raise_error(StandardError) }
145
+ end
146
+
147
+ context "without a name" do
148
+ let(:migration) do
149
+ <<~RUBY
150
+ create_sequence
151
+ RUBY
152
+ end
153
+
154
+ it { is_expected.to fail_validation.because(/name can't be blank/i) }
155
+ end
156
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe ActiveRecord::Migration, "#drop_sequence" do
4
+ before_all { run_migration("create_schema :app") }
5
+ before { run_migration(old_snippet) }
6
+
7
+ let(:old_snippet) do
8
+ <<~RUBY
9
+ create_sequence "app.global_num", as: "integer" do |s|
10
+ s.increment_by 2
11
+ s.min_value 0
12
+ s.max_value 2000
13
+ s.start_with 1
14
+ s.cache 10
15
+ s.cycle true
16
+ s.comment "Sequence for global numbers (odds then evens)"
17
+ end
18
+ RUBY
19
+ end
20
+
21
+ context "with a full definition" do
22
+ let(:migration) do
23
+ <<~RUBY
24
+ drop_sequence "app.global_num", as: "integer" do |s|
25
+ s.increment_by 2
26
+ s.min_value 0
27
+ s.max_value 2000
28
+ s.start_with 1
29
+ s.cache 10
30
+ s.cycle true
31
+ s.comment "Sequence for global numbers (odds then evens)"
32
+ end
33
+ RUBY
34
+ end
35
+
36
+ its(:execution) { is_expected.to remove(old_snippet).from_schema }
37
+ its(:inversion) { is_expected.not_to change_schema }
38
+ end
39
+
40
+ context "with a minimal definition" do
41
+ let(:migration) do
42
+ <<~RUBY
43
+ drop_sequence "app.global_num"
44
+ RUBY
45
+ end
46
+ let(:new_snippet) do
47
+ <<~RUBY
48
+ create_sequence "app.global_num"
49
+ RUBY
50
+ end
51
+
52
+ its(:execution) { is_expected.to remove(old_snippet).from_schema }
53
+ its(:inversion) { is_expected.to remove(old_snippet).from_schema }
54
+ its(:inversion) { is_expected.to insert(new_snippet).into_schema }
55
+ end
56
+
57
+ context "when the sequence was absent" do
58
+ before { run_migration(migration) }
59
+
60
+ context "without the `:if_exists` option" do
61
+ let(:migration) do
62
+ <<~RUBY
63
+ drop_sequence "app.global_num"
64
+ RUBY
65
+ end
66
+
67
+ its(:execution) { is_expected.to raise_error(StandardError) }
68
+ end
69
+
70
+ context "with the `if_exists: true` option" do
71
+ let(:migration) do
72
+ <<~RUBY
73
+ drop_sequence "app.global_num", if_exists: true
74
+ RUBY
75
+ end
76
+
77
+ its(:execution) { is_expected.not_to change_schema }
78
+ it { is_expected.to be_irreversible.because_of(/if_exists: true/i) }
79
+ end
80
+ end
81
+
82
+ context "with the force: :cascade option" do
83
+ let(:migration) do
84
+ <<~RUBY
85
+ drop_sequence "app.global_num", force: :cascade
86
+ RUBY
87
+ end
88
+
89
+ its(:execution) { is_expected.to remove(old_snippet).from_schema }
90
+ it { is_expected.to be_irreversible.because_of(/force: :cascade/i) }
91
+ end
92
+
93
+ context "without a name" do
94
+ let(:migration) do
95
+ <<~RUBY
96
+ drop_sequence
97
+ RUBY
98
+ end
99
+
100
+ it { is_expected.to fail_validation.because(/name can't be blank/i) }
101
+ end
102
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe ActiveRecord::Migration, "#rename_sequence" do
4
+ before_all { run_migration "create_schema :seq" }
5
+ before { run_migration(old_snippet) }
6
+
7
+ let(:old_snippet) do
8
+ <<~RUBY
9
+ create_sequence "global_num" do |s|
10
+ s.increment_by 2
11
+ s.min_value 0
12
+ s.max_value 2000
13
+ s.start_with 1
14
+ s.cache 10
15
+ s.cycle true
16
+ s.comment "Sequence for global numbers (odds then evens)"
17
+ end
18
+ RUBY
19
+ end
20
+
21
+ context "with a new name" do
22
+ let(:migration) do
23
+ <<~RUBY
24
+ rename_sequence "global_num", to: "seq.global_number"
25
+ RUBY
26
+ end
27
+ let(:new_snippet) do
28
+ <<~RUBY
29
+ create_sequence "seq.global_number" do |s|
30
+ s.increment_by 2
31
+ s.min_value 0
32
+ s.max_value 2000
33
+ s.start_with 1
34
+ s.cache 10
35
+ s.cycle true
36
+ s.comment "Sequence for global numbers (odds then evens)"
37
+ end
38
+ RUBY
39
+ end
40
+
41
+ its(:execution) { is_expected.to remove(old_snippet).from_schema }
42
+ its(:execution) { is_expected.to insert(new_snippet).into_schema }
43
+ its(:inversion) { is_expected.not_to change_schema }
44
+ end
45
+
46
+ context "when sequence was absent" do
47
+ let(:old_snippet) { "" }
48
+
49
+ context "without the `:if_exists` option" do
50
+ let(:migration) do
51
+ <<~RUBY
52
+ rename_sequence "global_num", to: "global_number"
53
+ RUBY
54
+ end
55
+
56
+ its(:execution) { is_expected.to raise_error(StandardError) }
57
+ end
58
+
59
+ context "with the `if_exists: true` option" do
60
+ let(:migration) do
61
+ <<~RUBY
62
+ rename_sequence "global_num", to: "global_number", if_exists: true
63
+ RUBY
64
+ end
65
+
66
+ its(:execution) { is_expected.not_to change_schema }
67
+ it { is_expected.to be_irreversible.because_of(/if_exists: true/i) }
68
+ end
69
+ end
70
+
71
+ context "with the same name" do
72
+ let(:migration) do
73
+ <<~RUBY
74
+ rename_sequence "global_num", to: "global_num"
75
+ RUBY
76
+ end
77
+
78
+ it { is_expected.to fail_validation.because(/new name must be different/i) }
79
+ end
80
+
81
+ context "without new name" do
82
+ let(:migration) do
83
+ <<~RUBY
84
+ rename_sequence "global_num"
85
+ RUBY
86
+ end
87
+
88
+ it { is_expected.to fail_validation.because(/new name can't be blank/i) }
89
+ end
90
+
91
+ context "without current name" do
92
+ let(:migration) do
93
+ <<~RUBY
94
+ rename_sequence to: "seq.global_number"
95
+ RUBY
96
+ end
97
+
98
+ it { is_expected.to fail_validation.because(/name can't be blank/i) }
99
+ end
100
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pg_trunk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kozin
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-01-20 00:00:00.000000000 Z
11
+ date: 2022-01-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -174,6 +174,12 @@ files:
174
174
  - lib/pg_trunk/operations/rules/create_rule.rb
175
175
  - lib/pg_trunk/operations/rules/drop_rule.rb
176
176
  - lib/pg_trunk/operations/rules/rename_rule.rb
177
+ - lib/pg_trunk/operations/sequences.rb
178
+ - lib/pg_trunk/operations/sequences/base.rb
179
+ - lib/pg_trunk/operations/sequences/change_sequence.rb
180
+ - lib/pg_trunk/operations/sequences/create_sequence.rb
181
+ - lib/pg_trunk/operations/sequences/drop_sequence.rb
182
+ - lib/pg_trunk/operations/sequences/rename_sequence.rb
177
183
  - lib/pg_trunk/operations/statistics.rb
178
184
  - lib/pg_trunk/operations/statistics/base.rb
179
185
  - lib/pg_trunk/operations/statistics/create_statistics.rb
@@ -246,6 +252,10 @@ files:
246
252
  - spec/operations/rules/create_rule_spec.rb
247
253
  - spec/operations/rules/drop_rule_spec.rb
248
254
  - spec/operations/rules/rename_rule_spec.rb
255
+ - spec/operations/sequences/change_sequence_spec.rb
256
+ - spec/operations/sequences/create_sequence_spec.rb
257
+ - spec/operations/sequences/drop_sequence_spec.rb
258
+ - spec/operations/sequences/rename_sequence_spec.rb
249
259
  - spec/operations/statistics/create_statistics_spec.rb
250
260
  - spec/operations/statistics/drop_statistics_spec.rb
251
261
  - spec/operations/statistics/rename_statistics_spec.rb