pg_trunk 0.1.3 → 0.2.0

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