pg_easy_replicate 0.2.1 → 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: 2a39a74d7e35cec6dce951d23eff51972e22811779ab8637edfa989567e62620
4
- data.tar.gz: 6a81b795c7968b5d5c218f35084cdf40bf558271fc8e290ef75fe28a570ce9b6
3
+ metadata.gz: efcbf369a4410abd783ea1dba49ead97fa471705842df4a35d28864349b1c70c
4
+ data.tar.gz: 676d97aa0c551dd0f9d58c3bccb229951bcb2dfc34653995375c359718bc1d7f
5
5
  SHA512:
6
- metadata.gz: f9af65ec14a25975af9fab3efa884d91aa90a43aae74b355370608b3c5e61453f6fd71dd333bd8917193f8b90ec86965464893f9aae2f1a965f2b40eaeebc882
7
- data.tar.gz: 9643dc3de2102b82ab29d085e90b3dfbbfa923d0c3324d91e1bff8fa7082a8136263e59566559cabbf230a3094648a2a52152912b24dd760ffc3c727b5a11f39
6
+ metadata.gz: 774ef530f4be4b409fdf2d4956a44deb09aebf59c8ef480074d426ca4c72006673c7a6915d860e61846fb00df8e14fa9534e920f5802873ed727e37564197b25
7
+ data.tar.gz: 50055941c3a83351b22ceaeb539b807a7cec5e3130732944c63e8a5440a7707a273a546681b1d581d4f79d9fba616998fc48cffc009e401bfc570e76e27064f0
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- pg_easy_replicate (0.2.1)
4
+ pg_easy_replicate (0.2.2)
5
5
  ougai (~> 2.0.0)
6
6
  pg (~> 1.5.3)
7
7
  sequel (>= 5.69, < 5.77)
@@ -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
@@ -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.1"
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.1
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: 2024-01-20 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