pg_easy_replicate 0.2.0 → 0.2.2

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: da825eb796e5b827c9197440301d841f725a5bbaeac74b066fea67b5bbf039ac
4
- data.tar.gz: 68da98e57eab118b7aea8f4e83171eaa8b18bd66a86bd5f39fb901f5aef9bfda
3
+ metadata.gz: efcbf369a4410abd783ea1dba49ead97fa471705842df4a35d28864349b1c70c
4
+ data.tar.gz: 676d97aa0c551dd0f9d58c3bccb229951bcb2dfc34653995375c359718bc1d7f
5
5
  SHA512:
6
- metadata.gz: e04ca73e6f6b63f68a37389cb788bc7a6fddd80aa75fa433d297d4a600b1929ee06341e456b8b2d12a0cb03952824c02dca63033f40675948c250bcba2849b2a
7
- data.tar.gz: b0617c3d5d2defeab8f576eb6cceb1845c8c7fe7478a064f2109fbd4e63ebcf58867bd2557221810c53778e62806bacd65e92470fd368f96c5d38405bee4efec
6
+ metadata.gz: 774ef530f4be4b409fdf2d4956a44deb09aebf59c8ef480074d426ca4c72006673c7a6915d860e61846fb00df8e14fa9534e920f5802873ed727e37564197b25
7
+ data.tar.gz: 50055941c3a83351b22ceaeb539b807a7cec5e3130732944c63e8a5440a7707a273a546681b1d581d4f79d9fba616998fc48cffc009e401bfc570e76e27064f0
data/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ ## [0.2.1] - 2023-12-29
2
+
3
+ - Don't attempt to drop and recreate unique indices - #88
4
+ - Dependency updates
5
+
6
+ ## [0.2.0] - 2023-12-29
7
+
8
+ - Recreate indices post COPY, once all tables are in replicating mode - #81
9
+
1
10
  ## [0.1.12] - 2023-12-13
2
11
 
3
12
  - Bump rubocop-rspec from 2.24.1 to 2.25.0 - #65
data/Gemfile.lock CHANGED
@@ -1,31 +1,31 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- pg_easy_replicate (0.2.0)
4
+ pg_easy_replicate (0.2.2)
5
5
  ougai (~> 2.0.0)
6
6
  pg (~> 1.5.3)
7
- sequel (>= 5.69, < 5.76)
7
+ sequel (>= 5.69, < 5.77)
8
8
  thor (>= 1.2.2, < 1.4.0)
9
9
 
10
10
  GEM
11
11
  remote: https://rubygems.org/
12
12
  specs:
13
13
  ast (2.4.2)
14
- bigdecimal (3.1.4)
14
+ bigdecimal (3.1.5)
15
15
  coderay (1.1.3)
16
16
  diff-lcs (1.5.0)
17
17
  haml (6.1.1)
18
18
  temple (>= 0.8.2)
19
19
  thor
20
20
  tilt
21
- json (2.7.0)
21
+ json (2.7.1)
22
22
  language_server-protocol (3.17.0.3)
23
23
  method_source (1.0.0)
24
24
  oj (3.14.3)
25
25
  ougai (2.0.0)
26
26
  oj (~> 3.10)
27
- parallel (1.23.0)
28
- parser (3.2.2.4)
27
+ parallel (1.24.0)
28
+ parser (3.3.0.4)
29
29
  ast (~> 2.4.1)
30
30
  racc
31
31
  pg (1.5.4)
@@ -37,7 +37,7 @@ GEM
37
37
  rainbow (3.1.1)
38
38
  rake (13.1.0)
39
39
  rbs (3.1.0)
40
- regexp_parser (2.8.2)
40
+ regexp_parser (2.9.0)
41
41
  rexml (3.2.6)
42
42
  rspec (3.12.0)
43
43
  rspec-core (~> 3.12.0)
@@ -52,11 +52,11 @@ GEM
52
52
  diff-lcs (>= 1.2.0, < 2.0)
53
53
  rspec-support (~> 3.12.0)
54
54
  rspec-support (3.12.0)
55
- rubocop (1.58.0)
55
+ rubocop (1.60.1)
56
56
  json (~> 2.3)
57
57
  language_server-protocol (>= 3.17.0)
58
58
  parallel (~> 1.10)
59
- parser (>= 3.2.2.4)
59
+ parser (>= 3.3.0.2)
60
60
  rainbow (>= 2.2.2, < 4.0)
61
61
  regexp_parser (>= 1.8, < 3.0)
62
62
  rexml (>= 3.2.5, < 4.0)
@@ -65,23 +65,23 @@ GEM
65
65
  unicode-display_width (>= 2.4.0, < 3.0)
66
66
  rubocop-ast (1.30.0)
67
67
  parser (>= 3.2.1.0)
68
- rubocop-capybara (2.19.0)
68
+ rubocop-capybara (2.20.0)
69
+ rubocop (~> 1.41)
70
+ rubocop-factory_bot (2.25.1)
69
71
  rubocop (~> 1.41)
70
- rubocop-factory_bot (2.24.0)
71
- rubocop (~> 1.33)
72
72
  rubocop-packaging (0.5.2)
73
73
  rubocop (>= 1.33, < 2.0)
74
- rubocop-performance (1.19.1)
75
- rubocop (>= 1.7.0, < 2.0)
76
- rubocop-ast (>= 0.4.0)
74
+ rubocop-performance (1.20.2)
75
+ rubocop (>= 1.48.1, < 2.0)
76
+ rubocop-ast (>= 1.30.0, < 2.0)
77
77
  rubocop-rake (0.6.0)
78
78
  rubocop (~> 1.0)
79
- rubocop-rspec (2.25.0)
79
+ rubocop-rspec (2.26.1)
80
80
  rubocop (~> 1.40)
81
81
  rubocop-capybara (~> 2.17)
82
82
  rubocop-factory_bot (~> 2.22)
83
83
  ruby-progressbar (1.13.0)
84
- sequel (5.75.0)
84
+ sequel (5.76.0)
85
85
  bigdecimal
86
86
  syntax_tree (6.2.0)
87
87
  prettier_print (>= 1.2.0)
@@ -16,10 +16,20 @@ module PgEasyReplicate
16
16
  aliases: "-c",
17
17
  boolean: true,
18
18
  desc: "Copy schema to the new database"
19
+ method_option :tables,
20
+ aliases: "-t",
21
+ desc:
22
+ "Comma separated list of table names. Default: All tables"
23
+ method_option :schema_name,
24
+ aliases: "-s",
25
+ desc:
26
+ "Name of the schema tables are in, only required if passing list of tables"
19
27
  def config_check
20
28
  PgEasyReplicate.assert_config(
21
29
  special_user_role: options[:special_user_role],
22
30
  copy_schema: options[:copy_schema],
31
+ tables: options[:tables],
32
+ schema_name: options[:schema_name],
23
33
  )
24
34
 
25
35
  puts "✅ Config is looking good."
@@ -72,5 +72,32 @@ module PgEasyReplicate
72
72
  raise(msg) if test_env?
73
73
  abort(msg)
74
74
  end
75
+
76
+ def determine_tables(conn_string:, list: "", schema: nil)
77
+ schema ||= "public"
78
+
79
+ tables = list&.split(",") || []
80
+ if tables.size > 0
81
+ tables
82
+ else
83
+ list_all_tables(schema: schema, conn_string: conn_string)
84
+ end
85
+ end
86
+
87
+ def list_all_tables(schema:, conn_string:)
88
+ Query
89
+ .run(
90
+ query:
91
+ "SELECT table_name
92
+ FROM information_schema.tables
93
+ WHERE table_schema = '#{schema}' AND
94
+ table_type = 'BASE TABLE'
95
+ ORDER BY table_name",
96
+ connection_url: conn_string,
97
+ user: db_user(conn_string),
98
+ )
99
+ .map(&:values)
100
+ .flatten
101
+ end
75
102
  end
76
103
  end
@@ -17,7 +17,7 @@ module PgEasyReplicate
17
17
  tables: tables,
18
18
  schema: schema,
19
19
  ).each do |index|
20
- drop_sql = "DROP INDEX CONCURRENTLY #{index[:index_name]};"
20
+ drop_sql = "DROP INDEX CONCURRENTLY #{schema}.#{index[:index_name]};"
21
21
 
22
22
  Query.run(
23
23
  query: drop_sql,
@@ -56,8 +56,8 @@ module PgEasyReplicate
56
56
  end
57
57
 
58
58
  def self.fetch_indices(conn_string:, tables:, schema:)
59
- return [] if tables.split(",").empty?
60
- table_list = tables.split(",").map { |table| "'#{table}'" }.join(",")
59
+ return [] if tables.empty?
60
+ table_list = tables.map { |table| "'#{table}'" }.join(",")
61
61
 
62
62
  sql = <<-SQL
63
63
  SELECT
@@ -75,6 +75,7 @@ module PgEasyReplicate
75
75
  AND n.oid = t.relnamespace
76
76
  AND t.relkind = 'r' -- only find indexes of tables
77
77
  AND ix.indisprimary = FALSE -- exclude primary keys
78
+ AND ix.indisunique = FALSE -- exclude unique indexes
78
79
  AND n.nspname = '#{schema}'
79
80
  AND t.relname IN (#{table_list})
80
81
  ORDER BY
@@ -46,7 +46,7 @@ module PgEasyReplicate
46
46
 
47
47
  Group.create(
48
48
  name: options[:group_name],
49
- table_names: tables,
49
+ table_names: tables.join(","),
50
50
  schema_name: schema_name,
51
51
  started_at: Time.now.utc,
52
52
  recreate_indices_post_copy: options[:recreate_indices_post_copy],
@@ -63,7 +63,7 @@ module PgEasyReplicate
63
63
  else
64
64
  Group.create(
65
65
  name: options[:group_name],
66
- table_names: tables,
66
+ table_names: tables.join(","),
67
67
  schema_name: schema_name,
68
68
  started_at: Time.now.utc,
69
69
  failed_at: Time.now.utc,
@@ -92,42 +92,24 @@ module PgEasyReplicate
92
92
  schema:,
93
93
  group_name:,
94
94
  conn_string:,
95
- tables: ""
95
+ tables: []
96
96
  )
97
97
  logger.info(
98
98
  "Adding tables up publication",
99
99
  { publication_name: publication_name(group_name) },
100
100
  )
101
101
 
102
- tables
103
- .split(",")
104
- .map do |table_name|
105
- Query.run(
106
- query:
107
- "ALTER PUBLICATION #{quote_ident(publication_name(group_name))}
108
- ADD TABLE #{quote_ident(table_name)}",
109
- connection_url: conn_string,
110
- schema: schema,
111
- )
112
- end
113
- rescue => e
114
- raise "Unable to add tables to publication: #{e.message}"
115
- end
116
-
117
- def list_all_tables(schema:, conn_string:)
118
- Query
119
- .run(
102
+ tables.map do |table_name|
103
+ Query.run(
120
104
  query:
121
- "SELECT table_name
122
- FROM information_schema.tables
123
- WHERE table_schema = '#{schema}' AND
124
- table_type = 'BASE TABLE'
125
- ORDER BY table_name",
105
+ "ALTER PUBLICATION #{quote_ident(publication_name(group_name))}
106
+ ADD TABLE #{quote_ident(table_name)}",
126
107
  connection_url: conn_string,
108
+ schema: schema,
127
109
  )
128
- .map(&:values)
129
- .flatten
130
- .join(",")
110
+ end
111
+ rescue => e
112
+ raise "Unable to add tables to publication: #{e.message}"
131
113
  end
132
114
 
133
115
  def drop_publication(group_name:, conn_string:)
@@ -225,10 +207,11 @@ module PgEasyReplicate
225
207
  lag_delta_size: nil
226
208
  )
227
209
  group = Group.find(group_name)
210
+ tables_list = group[:table_names].split(",")
228
211
 
229
212
  run_vacuum_analyze(
230
213
  conn_string: target_conn_string,
231
- tables: group[:table_names],
214
+ tables: tables_list,
232
215
  schema: group[:schema_name],
233
216
  )
234
217
 
@@ -239,7 +222,7 @@ module PgEasyReplicate
239
222
  IndexManager.recreate_indices(
240
223
  source_conn_string: source_db_url,
241
224
  target_conn_string: target_db_url,
242
- tables: group[:table_names],
225
+ tables: tables_list,
243
226
  schema: group[:schema_name],
244
227
  )
245
228
  end
@@ -257,7 +240,7 @@ module PgEasyReplicate
257
240
  # Run vacuum analyze to refresh the planner post switchover
258
241
  run_vacuum_analyze(
259
242
  conn_string: target_conn_string,
260
- tables: group[:table_names],
243
+ tables: tables_list,
261
244
  schema: group[:schema_name],
262
245
  )
263
246
  drop_subscription(
@@ -369,21 +352,19 @@ module PgEasyReplicate
369
352
  end
370
353
 
371
354
  def run_vacuum_analyze(conn_string:, tables:, schema:)
372
- tables
373
- .split(",")
374
- .each do |t|
375
- logger.info(
376
- "Running vacuum analyze on #{t}",
377
- schema: schema,
378
- table: t,
379
- )
380
- Query.run(
381
- query: "VACUUM VERBOSE ANALYZE #{t}",
382
- connection_url: conn_string,
383
- schema: schema,
384
- transaction: false,
385
- )
386
- end
355
+ tables.each do |t|
356
+ logger.info(
357
+ "Running vacuum analyze on #{t}",
358
+ schema: schema,
359
+ table: t,
360
+ )
361
+ Query.run(
362
+ query: "VACUUM VERBOSE ANALYZE #{t}",
363
+ connection_url: conn_string,
364
+ schema: schema,
365
+ transaction: false,
366
+ )
367
+ end
387
368
  rescue => e
388
369
  raise "Unable to run vacuum and analyze: #{e.message}"
389
370
  end
@@ -391,16 +372,6 @@ module PgEasyReplicate
391
372
  def mark_switchover_complete(group_name)
392
373
  Group.update(group_name: group_name, switchover_completed_at: Time.now)
393
374
  end
394
-
395
- private
396
-
397
- def determine_tables(schema:, conn_string:, list: "")
398
- tables = list&.split(",") || []
399
- unless tables.size > 0
400
- return list_all_tables(schema: schema, conn_string: conn_string)
401
- end
402
- ""
403
- end
404
375
  end
405
376
  end
406
377
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgEasyReplicate
4
- VERSION = "0.2.0"
4
+ VERSION = "0.2.2"
5
5
  end
@@ -26,7 +26,12 @@ module PgEasyReplicate
26
26
  extend Helper
27
27
 
28
28
  class << self
29
- def config(special_user_role: nil, copy_schema: false)
29
+ def config(
30
+ special_user_role: nil,
31
+ copy_schema: false,
32
+ tables: "",
33
+ schema_name: nil
34
+ )
30
35
  abort_with("SOURCE_DB_URL is missing") if source_db_url.nil?
31
36
  abort_with("TARGET_DB_URL is missing") if target_db_url.nil?
32
37
 
@@ -56,15 +61,31 @@ module PgEasyReplicate
56
61
  user: db_user(target_db_url),
57
62
  ),
58
63
  pg_dump_exists: pg_dump_exists,
64
+ tables_have_replica_identity:
65
+ tables_have_replica_identity?(
66
+ conn_string: source_db_url,
67
+ tables: tables,
68
+ schema_name: schema_name,
69
+ ),
59
70
  }
60
71
  rescue => e
61
72
  abort_with("Unable to check config: #{e.message}")
62
73
  end
63
74
  end
64
75
 
65
- def assert_config(special_user_role: nil, copy_schema: false)
76
+ def assert_config(
77
+ special_user_role: nil,
78
+ copy_schema: false,
79
+ tables: "",
80
+ schema_name: nil
81
+ )
66
82
  config_hash =
67
- config(special_user_role: special_user_role, copy_schema: copy_schema)
83
+ config(
84
+ special_user_role: special_user_role,
85
+ copy_schema: copy_schema,
86
+ tables: tables,
87
+ schema_name: schema_name,
88
+ )
68
89
 
69
90
  if copy_schema && !config_hash.dig(:pg_dump_exists)
70
91
  abort_with("pg_dump must exist if copy_schema (-c) is passed")
@@ -82,6 +103,19 @@ module PgEasyReplicate
82
103
  abort_with("User on source database does not have super user privilege")
83
104
  end
84
105
 
106
+ if tables.split(",").size > 0 && (schema_name.nil? || schema_name == "")
107
+ abort_with("Schema name is required if tables are passed")
108
+ end
109
+
110
+ unless config_hash.dig(:tables_have_replica_identity)
111
+ abort_with(
112
+ "Ensure all tables involved in logical replication have an appropriate replica identity set. This can be done using:
113
+ 1. Default (Primary Key): `ALTER TABLE table_name REPLICA IDENTITY DEFAULT;`
114
+ 2. Unique Index: `ALTER TABLE table_name REPLICA IDENTITY USING INDEX index_name;`
115
+ 3. Full (All Columns): `ALTER TABLE table_name REPLICA IDENTITY FULL;`",
116
+ )
117
+ end
118
+
85
119
  return if config_hash.dig(:target_db_is_super_user)
86
120
  abort_with("User on target database does not have super user privilege")
87
121
  end
@@ -352,5 +386,47 @@ module PgEasyReplicate
352
386
  )
353
387
  .any? { |q| q[:username] == user }
354
388
  end
389
+
390
+ def tables_have_replica_identity?(
391
+ conn_string:,
392
+ tables: "",
393
+ schema_name: nil
394
+ )
395
+ schema_name ||= "public"
396
+
397
+ table_list =
398
+ determine_tables(
399
+ schema: schema_name,
400
+ conn_string: source_db_url,
401
+ list: tables,
402
+ )
403
+ return false if table_list.empty?
404
+
405
+ formatted_table_list = table_list.map { |table| "'#{table}'" }.join(", ")
406
+
407
+ sql = <<~SQL
408
+ SELECT t.relname AS table_name,
409
+ CASE
410
+ WHEN t.relreplident = 'd' THEN 'default'
411
+ WHEN t.relreplident = 'n' THEN 'nothing'
412
+ WHEN t.relreplident = 'i' THEN 'index'
413
+ WHEN t.relreplident = 'f' THEN 'full'
414
+ END AS replica_identity
415
+ FROM pg_class t
416
+ JOIN pg_namespace ns ON t.relnamespace = ns.oid
417
+ WHERE ns.nspname = '#{schema_name}'
418
+ AND t.relkind = 'r'
419
+ AND t.relname IN (#{formatted_table_list})
420
+ SQL
421
+
422
+ results =
423
+ Query.run(
424
+ query: sql,
425
+ connection_url: conn_string,
426
+ user: db_user(conn_string),
427
+ )
428
+
429
+ results.all? { |r| r[:replica_identity] != "nothing" }
430
+ end
355
431
  end
356
432
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pg_easy_replicate
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shayon Mukherjee
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-12-29 00:00:00.000000000 Z
11
+ date: 2024-01-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ougai
@@ -47,7 +47,7 @@ dependencies:
47
47
  version: '5.69'
48
48
  - - "<"
49
49
  - !ruby/object:Gem::Version
50
- version: '5.76'
50
+ version: '5.77'
51
51
  type: :runtime
52
52
  prerelease: false
53
53
  version_requirements: !ruby/object:Gem::Requirement
@@ -57,7 +57,7 @@ dependencies:
57
57
  version: '5.69'
58
58
  - - "<"
59
59
  - !ruby/object:Gem::Version
60
- version: '5.76'
60
+ version: '5.77'
61
61
  - !ruby/object:Gem::Dependency
62
62
  name: thor
63
63
  requirement: !ruby/object:Gem::Requirement