pg_easy_replicate 0.2.1 → 0.2.2

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: 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