pg_ha_migrations 1.7.0 → 2.0.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.
@@ -0,0 +1,112 @@
1
+ module PgHaMigrations
2
+ class LockMode
3
+ include Comparable
4
+
5
+ MODE_CONFLICTS = ActiveSupport::OrderedHash.new
6
+
7
+ MODE_CONFLICTS[:access_share] = %i[
8
+ access_exclusive
9
+ ]
10
+
11
+ MODE_CONFLICTS[:row_share] = %i[
12
+ exclusive
13
+ access_exclusive
14
+ ]
15
+
16
+ MODE_CONFLICTS[:row_exclusive] = %i[
17
+ share
18
+ share_row_exclusive
19
+ exclusive
20
+ access_exclusive
21
+ ]
22
+
23
+ MODE_CONFLICTS[:share_update_exclusive] = %i[
24
+ share_update_exclusive
25
+ share
26
+ share_row_exclusive
27
+ exclusive
28
+ access_exclusive
29
+ ]
30
+
31
+ MODE_CONFLICTS[:share] = %i[
32
+ row_exclusive
33
+ share_update_exclusive
34
+ share_row_exclusive
35
+ exclusive
36
+ access_exclusive
37
+ ]
38
+
39
+ MODE_CONFLICTS[:share_row_exclusive] = %i[
40
+ row_exclusive
41
+ share_update_exclusive
42
+ share
43
+ share_row_exclusive
44
+ exclusive
45
+ access_exclusive
46
+ ]
47
+
48
+ MODE_CONFLICTS[:exclusive] = %i[
49
+ row_share
50
+ row_exclusive
51
+ share_update_exclusive
52
+ share
53
+ share_row_exclusive
54
+ exclusive
55
+ access_exclusive
56
+ ]
57
+
58
+ MODE_CONFLICTS[:access_exclusive] = %i[
59
+ access_share
60
+ row_share
61
+ row_exclusive
62
+ share_update_exclusive
63
+ share
64
+ share_row_exclusive
65
+ exclusive
66
+ access_exclusive
67
+ ]
68
+
69
+ attr_reader :mode
70
+
71
+ delegate :to_s, to: :mode
72
+
73
+ def initialize(mode)
74
+ @mode = mode
75
+ .to_s
76
+ .underscore
77
+ .delete_suffix("_lock")
78
+ .to_sym
79
+
80
+ if !MODE_CONFLICTS.keys.include?(@mode)
81
+ raise ArgumentError, "Unrecognized lock mode #{@mode.inspect}. Valid modes: #{MODE_CONFLICTS.keys}"
82
+ end
83
+ end
84
+
85
+ def to_sql
86
+ mode
87
+ .to_s
88
+ .upcase
89
+ .gsub("_", " ")
90
+ end
91
+
92
+ def <=>(other)
93
+ MODE_CONFLICTS.keys.index(mode) <=> MODE_CONFLICTS.keys.index(other.mode)
94
+ end
95
+
96
+ def eql?(other)
97
+ other.is_a?(LockMode) && mode == other.mode
98
+ end
99
+
100
+ def ==(other)
101
+ eql?(other)
102
+ end
103
+
104
+ def hash
105
+ mode.hash
106
+ end
107
+
108
+ def conflicts_with?(other)
109
+ MODE_CONFLICTS[mode].include?(other.mode)
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,224 @@
1
+ module PgHaMigrations
2
+ # This object represents a pointer to an actual relation in Postgres.
3
+ # The mode attribute is optional metadata which can represent a lock
4
+ # that has already been acquired or a potential lock that we are
5
+ # looking to acquire.
6
+ Relation = Struct.new(:name, :schema, :mode) do
7
+ def self.connection
8
+ ActiveRecord::Base.connection
9
+ end
10
+
11
+ delegate :inspect, to: :name
12
+ delegate :connection, to: :class
13
+
14
+ def initialize(name, schema, mode=nil)
15
+ super(name, schema)
16
+
17
+ self.mode = LockMode.new(mode) if mode.present?
18
+ end
19
+
20
+ def fully_qualified_name
21
+ @fully_qualified_name ||= [
22
+ PG::Connection.quote_ident(schema),
23
+ PG::Connection.quote_ident(name),
24
+ ].join(".")
25
+ end
26
+
27
+ def present?
28
+ name.present? && schema.present?
29
+ end
30
+
31
+ def conflicts_with?(other)
32
+ eql?(other) && (
33
+ mode.nil? || other.mode.nil? || mode.conflicts_with?(other.mode)
34
+ )
35
+ end
36
+
37
+ # Some code paths need to compare lock modes, while others just need
38
+ # to determine if the relation is the same object in Postgres, so
39
+ # equality here is simply looking at the relation name / schema.
40
+ # To also compare lock modes, #conflicts_with? is used.
41
+ def eql?(other)
42
+ other.is_a?(Relation) && hash == other.hash
43
+ end
44
+
45
+ def ==(other)
46
+ eql?(other)
47
+ end
48
+
49
+ def hash
50
+ [name, schema].hash
51
+ end
52
+ end
53
+
54
+ class Table < Relation
55
+ def self.from_table_name(table, mode=nil)
56
+ pg_name = ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(table.to_s)
57
+
58
+ schema_conditional = if pg_name.schema
59
+ "#{connection.quote(pg_name.schema)}"
60
+ else
61
+ "ANY (current_schemas(false))"
62
+ end
63
+
64
+ schema = connection.select_value(<<~SQL)
65
+ SELECT schemaname
66
+ FROM pg_tables
67
+ WHERE tablename = #{connection.quote(pg_name.identifier)} AND schemaname = #{schema_conditional}
68
+ ORDER BY array_position(current_schemas(false), schemaname)
69
+ LIMIT 1
70
+ SQL
71
+
72
+ raise UndefinedTableError, "Table #{pg_name.quoted} does not exist#{" in search path" unless pg_name.schema}" unless schema.present?
73
+
74
+ new(pg_name.identifier, schema, mode)
75
+ end
76
+
77
+ def natively_partitioned?
78
+ return @natively_partitioned if defined?(@natively_partitioned)
79
+
80
+ @natively_partitioned = !!connection.select_value(<<~SQL)
81
+ SELECT true
82
+ FROM pg_partitioned_table, pg_class, pg_namespace
83
+ WHERE pg_class.oid = pg_partitioned_table.partrelid
84
+ AND pg_class.relnamespace = pg_namespace.oid
85
+ AND pg_class.relname = #{connection.quote(name)}
86
+ AND pg_namespace.nspname = #{connection.quote(schema)}
87
+ SQL
88
+ end
89
+
90
+ def partitions(include_sub_partitions: false, include_self: false)
91
+ tables = connection.structs_from_sql(self.class, <<~SQL)
92
+ SELECT child.relname AS name, child_ns.nspname AS schema, NULLIF('#{mode}', '') AS mode
93
+ FROM pg_inherits
94
+ JOIN pg_class parent ON pg_inherits.inhparent = parent.oid
95
+ JOIN pg_class child ON pg_inherits.inhrelid = child.oid
96
+ JOIN pg_namespace parent_ns ON parent.relnamespace = parent_ns.oid
97
+ JOIN pg_namespace child_ns ON child.relnamespace = child_ns.oid
98
+ WHERE parent.relname = #{connection.quote(name)}
99
+ AND parent_ns.nspname = #{connection.quote(schema)}
100
+ ORDER BY child.oid -- Ensure consistent ordering for tests
101
+ SQL
102
+
103
+ if include_sub_partitions
104
+ sub_partitions = tables.each_with_object([]) do |table, arr|
105
+ arr.concat(table.partitions(include_sub_partitions: true))
106
+ end
107
+
108
+ tables.concat(sub_partitions)
109
+ end
110
+
111
+ tables.prepend(self) if include_self
112
+
113
+ tables
114
+ end
115
+
116
+ def check_constraints
117
+ connection.structs_from_sql(PgHaMigrations::CheckConstraint, <<~SQL)
118
+ SELECT conname AS name, pg_get_constraintdef(pg_constraint.oid) AS definition, convalidated AS validated
119
+ FROM pg_constraint, pg_class, pg_namespace
120
+ WHERE pg_class.oid = pg_constraint.conrelid
121
+ AND pg_class.relnamespace = pg_namespace.oid
122
+ AND pg_class.relname = #{connection.quote(name)}
123
+ AND pg_namespace.nspname = #{connection.quote(schema)}
124
+ AND pg_constraint.contype = 'c' -- 'c' stands for check constraints
125
+ SQL
126
+ end
127
+
128
+ def has_rows?
129
+ connection.select_value("SELECT EXISTS (SELECT 1 FROM #{fully_qualified_name} LIMIT 1)")
130
+ end
131
+
132
+ def total_bytes
133
+ connection.select_value(<<~SQL)
134
+ SELECT pg_total_relation_size(pg_class.oid)
135
+ FROM pg_class, pg_namespace
136
+ WHERE pg_class.relname = #{connection.quote(name)}
137
+ AND pg_namespace.nspname = #{connection.quote(schema)}
138
+ SQL
139
+ end
140
+ end
141
+
142
+ class Index < Relation
143
+ MAX_NAME_SIZE = 63 # bytes
144
+
145
+ def self.from_table_and_columns(table, columns)
146
+ name = connection.index_name(table.name, columns)
147
+
148
+ # modified from https://github.com/rails/rails/pull/47753
149
+ if name.bytesize > MAX_NAME_SIZE
150
+ hashed_identifier = "_#{OpenSSL::Digest::SHA256.hexdigest(name).first(10)}"
151
+ description = name.sub("index_#{table.name}_on", "idx_on")
152
+
153
+ short_limit = MAX_NAME_SIZE - hashed_identifier.bytesize
154
+ short_description = description.mb_chars.limit(short_limit).to_s
155
+
156
+ name = "#{short_description}#{hashed_identifier}"
157
+ end
158
+
159
+ new(name, table)
160
+ end
161
+
162
+ attr_accessor :table
163
+
164
+ def initialize(name, table)
165
+ super(name, table.schema)
166
+
167
+ self.table = table
168
+
169
+ connection.send(:validate_index_length!, table.name, name)
170
+ end
171
+
172
+ def valid?
173
+ !!connection.select_value(<<~SQL)
174
+ SELECT pg_index.indisvalid
175
+ FROM pg_index, pg_class, pg_namespace
176
+ WHERE pg_class.oid = pg_index.indexrelid
177
+ AND pg_class.relnamespace = pg_namespace.oid
178
+ AND pg_namespace.nspname = #{connection.quote(schema)}
179
+ AND pg_class.relname = #{connection.quote(name)}
180
+ SQL
181
+ end
182
+ end
183
+
184
+ class TableCollection
185
+ include Enumerable
186
+
187
+ attr_reader :raw_set
188
+
189
+ delegate :each, to: :raw_set
190
+ delegate :mode, to: :first
191
+
192
+ def self.from_table_names(tables, mode=nil)
193
+ new(tables) { |table| Table.from_table_name(table, mode) }
194
+ end
195
+
196
+ def initialize(tables, &blk)
197
+ @raw_set = tables.map(&blk).to_set
198
+
199
+ if raw_set.empty?
200
+ raise ArgumentError, "Expected a non-empty list of tables"
201
+ end
202
+
203
+ if raw_set.uniq(&:mode).size > 1
204
+ raise ArgumentError, "Expected all tables in collection to have the same lock mode"
205
+ end
206
+ end
207
+
208
+ def subset?(other)
209
+ raw_set.subset?(other.raw_set)
210
+ end
211
+
212
+ def to_sql
213
+ map(&:fully_qualified_name).join(", ")
214
+ end
215
+
216
+ def with_partitions
217
+ tables = flat_map do |table|
218
+ table.partitions(include_sub_partitions: true, include_self: true)
219
+ end
220
+
221
+ self.class.new(tables)
222
+ end
223
+ end
224
+ end