pcrd 0.1.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.
Files changed (72) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +24 -0
  3. data/LICENSE +21 -0
  4. data/README.md +614 -0
  5. data/bin/pcrd +7 -0
  6. data/lib/pcrd/advisory_lock.rb +50 -0
  7. data/lib/pcrd/apply/engine.rb +184 -0
  8. data/lib/pcrd/apply/worker.rb +97 -0
  9. data/lib/pcrd/backfill/batch.rb +158 -0
  10. data/lib/pcrd/backfill/engine.rb +153 -0
  11. data/lib/pcrd/checkpoint/store.rb +217 -0
  12. data/lib/pcrd/cli.rb +274 -0
  13. data/lib/pcrd/commands/analyze.rb +125 -0
  14. data/lib/pcrd/commands/cleanup.rb +112 -0
  15. data/lib/pcrd/commands/demo.rb +152 -0
  16. data/lib/pcrd/commands/readiness.rb +30 -0
  17. data/lib/pcrd/commands/status.rb +129 -0
  18. data/lib/pcrd/commands/verify.rb +172 -0
  19. data/lib/pcrd/config/add_column.rb +7 -0
  20. data/lib/pcrd/config/analyze_config.rb +8 -0
  21. data/lib/pcrd/config/column_spec.rb +10 -0
  22. data/lib/pcrd/config/connection.rb +7 -0
  23. data/lib/pcrd/config/cutover_config.rb +7 -0
  24. data/lib/pcrd/config/load_error.rb +7 -0
  25. data/lib/pcrd/config/loader.rb +158 -0
  26. data/lib/pcrd/config/migrate_config.rb +21 -0
  27. data/lib/pcrd/config/root.rb +9 -0
  28. data/lib/pcrd/config/schema.rb +62 -0
  29. data/lib/pcrd/config/table.rb +9 -0
  30. data/lib/pcrd/config/verify_config.rb +7 -0
  31. data/lib/pcrd/config.rb +7 -0
  32. data/lib/pcrd/connection/client.rb +129 -0
  33. data/lib/pcrd/connection/error.rb +7 -0
  34. data/lib/pcrd/connection/replication.rb +108 -0
  35. data/lib/pcrd/cutover/orchestrator.rb +108 -0
  36. data/lib/pcrd/cutover/sequences.rb +138 -0
  37. data/lib/pcrd/demo/generator.rb +214 -0
  38. data/lib/pcrd/demo/schema.rb +154 -0
  39. data/lib/pcrd/error.rb +12 -0
  40. data/lib/pcrd/migration/orchestrator.rb +272 -0
  41. data/lib/pcrd/monitor/lag.rb +107 -0
  42. data/lib/pcrd/options.rb +15 -0
  43. data/lib/pcrd/output/analyze_printer.rb +173 -0
  44. data/lib/pcrd/output/cutover_printer.rb +128 -0
  45. data/lib/pcrd/output/preflight_printer.rb +119 -0
  46. data/lib/pcrd/output/readiness_printer.rb +72 -0
  47. data/lib/pcrd/preflight.rb +331 -0
  48. data/lib/pcrd/readiness/manifest.rb +201 -0
  49. data/lib/pcrd/replication/consumer.rb +235 -0
  50. data/lib/pcrd/replication/error.rb +10 -0
  51. data/lib/pcrd/replication/pgoutput/messages.rb +68 -0
  52. data/lib/pcrd/replication/pgoutput/parser.rb +316 -0
  53. data/lib/pcrd/reporter/console.rb +46 -0
  54. data/lib/pcrd/reporter/null.rb +14 -0
  55. data/lib/pcrd/schema/column.rb +59 -0
  56. data/lib/pcrd/schema/ddl.rb +71 -0
  57. data/lib/pcrd/schema/diff_entry.rb +36 -0
  58. data/lib/pcrd/schema/differ.rb +175 -0
  59. data/lib/pcrd/schema/object_reader.rb +187 -0
  60. data/lib/pcrd/schema/packer.rb +90 -0
  61. data/lib/pcrd/schema/reader.rb +118 -0
  62. data/lib/pcrd/schema/setup.rb +143 -0
  63. data/lib/pcrd/schema/setup_error.rb +9 -0
  64. data/lib/pcrd/schema/table_not_found.rb +8 -0
  65. data/lib/pcrd/schema/type_registry.rb +116 -0
  66. data/lib/pcrd/sql.rb +55 -0
  67. data/lib/pcrd/transform/row_transformer.rb +69 -0
  68. data/lib/pcrd/transform/type_map.rb +209 -0
  69. data/lib/pcrd/transform/validator.rb +106 -0
  70. data/lib/pcrd/version.rb +5 -0
  71. data/lib/pcrd.rb +11 -0
  72. metadata +231 -0
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcrd
4
+ module Transform
5
+ # Runs pre-migration data validation queries on the source database.
6
+ #
7
+ # For each column in the migration spec that involves a validated cast,
8
+ # Validator queries the source table to confirm no rows would be rejected
9
+ # or silently truncated by the cast. Failures are collected and reported
10
+ # all at once so the operator can fix everything in one pass.
11
+ #
12
+ # Called by the preflight phase before any replication slot is created.
13
+ class Validator
14
+ ValidationFailure = Data.define(:table_name, :column_name, :source_type,
15
+ :target_type, :failing_count, :description,
16
+ :warn_only)
17
+
18
+ def initialize(source_pool)
19
+ @pool = source_pool
20
+ end
21
+
22
+ # Validates all columns in table_config against their source schema.
23
+ #
24
+ # source_columns: Array<Schema::Column> from Schema::Reader
25
+ # Returns Array<ValidationFailure> — empty means all checks passed.
26
+ # Raises on unexpected database errors.
27
+ def validate(table_config, source_columns)
28
+ failures = []
29
+ col_index = source_columns.each_with_object({}) { |c, h| h[c.name] = c }
30
+
31
+ (table_config.columns || {}).each do |src_name, col_spec|
32
+ next if col_spec.drop || col_spec.type.nil?
33
+
34
+ source_col = col_index[src_name.to_s]
35
+ next unless source_col
36
+
37
+ safety = TypeMap.cast_safety(source_col.type_name, col_spec.type)
38
+ next if %i[no_op always_safe].include?(safety)
39
+
40
+ if safety == :unsupported
41
+ failures << ValidationFailure.new(
42
+ table_name: table_config.name,
43
+ column_name: src_name,
44
+ source_type: source_col.display_type,
45
+ target_type: col_spec.type,
46
+ failing_count: nil,
47
+ description: "pcrd does not support this type transition — " \
48
+ "use a custom transform or perform it separately",
49
+ warn_only: false
50
+ )
51
+ next
52
+ end
53
+
54
+ # :validated — run the data check
55
+ result = run_check(table_config.name, src_name, source_col, col_spec.type)
56
+ failures << result if result
57
+ end
58
+
59
+ failures
60
+ end
61
+
62
+ private
63
+
64
+ def run_check(table_name, col_name, source_col, target_type)
65
+ rule = TypeMap.validated_rule(source_col.type_name, target_type)
66
+ return nil unless rule
67
+
68
+ quoted_col = Sql.quote_ident(col_name.to_s)
69
+ quoted_table = Sql.quote_table(table_name)
70
+
71
+ count = if rule[:check_expr] == :varchar_length_check
72
+ varchar_length_check(quoted_table, quoted_col, target_type)
73
+ elsif rule[:check_expr]
74
+ expr = rule[:check_expr].call(quoted_col)
75
+ result = @pool.exec("SELECT COUNT(*) FROM #{quoted_table} WHERE #{expr}")
76
+ result[0]["count"].to_i
77
+ else
78
+ 0 # warn_only rule with no SQL check
79
+ end
80
+
81
+ return nil if count.zero? && !rule[:warn_only]
82
+
83
+ ValidationFailure.new(
84
+ table_name: table_name,
85
+ column_name: col_name.to_s,
86
+ source_type: source_col.display_type,
87
+ target_type: target_type,
88
+ failing_count: rule[:check_expr] ? count : nil,
89
+ description: rule[:description],
90
+ warn_only: rule[:warn_only]
91
+ )
92
+ end
93
+
94
+ def varchar_length_check(quoted_table, quoted_col, target_type)
95
+ max_len = TypeMap.extract_length(target_type)
96
+ return 0 unless max_len
97
+
98
+ result = @pool.exec(
99
+ "SELECT COUNT(*) FROM #{quoted_table} WHERE length(#{quoted_col}) > $1",
100
+ [max_len]
101
+ )
102
+ result[0]["count"].to_i
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcrd
4
+ VERSION = "0.1.0"
5
+ end
data/lib/pcrd.rb ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zeitwerk"
4
+
5
+ loader = Zeitwerk::Loader.for_gem
6
+ loader.inflector.inflect "cli" => "CLI"
7
+ loader.inflector.inflect "ddl" => "DDL"
8
+ loader.setup
9
+
10
+ module Pcrd
11
+ end
metadata ADDED
@@ -0,0 +1,231 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pcrd
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Charles Harris
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: pg
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.5'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.5'
26
+ - !ruby/object:Gem::Dependency
27
+ name: thor
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.3'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.3'
40
+ - !ruby/object:Gem::Dependency
41
+ name: sqlite3
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '2.1'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '2.1'
54
+ - !ruby/object:Gem::Dependency
55
+ name: tty-table
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '0.12'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '0.12'
68
+ - !ruby/object:Gem::Dependency
69
+ name: tty-progressbar
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '0.18'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '0.18'
82
+ - !ruby/object:Gem::Dependency
83
+ name: pastel
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '0.8'
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '0.8'
96
+ - !ruby/object:Gem::Dependency
97
+ name: zeitwerk
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '2.6'
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '2.6'
110
+ - !ruby/object:Gem::Dependency
111
+ name: dry-schema
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '1.13'
117
+ type: :runtime
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '1.13'
124
+ description: |
125
+ pcrd migrates PostgreSQL tables to a new cluster using logical replication,
126
+ with support for column type changes, renames, additions, drops, and column
127
+ reordering with padding optimization. Designed for large tables where
128
+ ALTER TABLE would cause unacceptable downtime.
129
+ email:
130
+ - charris000@gmail.com
131
+ executables:
132
+ - pcrd
133
+ extensions: []
134
+ extra_rdoc_files: []
135
+ files:
136
+ - CHANGELOG.md
137
+ - LICENSE
138
+ - README.md
139
+ - bin/pcrd
140
+ - lib/pcrd.rb
141
+ - lib/pcrd/advisory_lock.rb
142
+ - lib/pcrd/apply/engine.rb
143
+ - lib/pcrd/apply/worker.rb
144
+ - lib/pcrd/backfill/batch.rb
145
+ - lib/pcrd/backfill/engine.rb
146
+ - lib/pcrd/checkpoint/store.rb
147
+ - lib/pcrd/cli.rb
148
+ - lib/pcrd/commands/analyze.rb
149
+ - lib/pcrd/commands/cleanup.rb
150
+ - lib/pcrd/commands/demo.rb
151
+ - lib/pcrd/commands/readiness.rb
152
+ - lib/pcrd/commands/status.rb
153
+ - lib/pcrd/commands/verify.rb
154
+ - lib/pcrd/config.rb
155
+ - lib/pcrd/config/add_column.rb
156
+ - lib/pcrd/config/analyze_config.rb
157
+ - lib/pcrd/config/column_spec.rb
158
+ - lib/pcrd/config/connection.rb
159
+ - lib/pcrd/config/cutover_config.rb
160
+ - lib/pcrd/config/load_error.rb
161
+ - lib/pcrd/config/loader.rb
162
+ - lib/pcrd/config/migrate_config.rb
163
+ - lib/pcrd/config/root.rb
164
+ - lib/pcrd/config/schema.rb
165
+ - lib/pcrd/config/table.rb
166
+ - lib/pcrd/config/verify_config.rb
167
+ - lib/pcrd/connection/client.rb
168
+ - lib/pcrd/connection/error.rb
169
+ - lib/pcrd/connection/replication.rb
170
+ - lib/pcrd/cutover/orchestrator.rb
171
+ - lib/pcrd/cutover/sequences.rb
172
+ - lib/pcrd/demo/generator.rb
173
+ - lib/pcrd/demo/schema.rb
174
+ - lib/pcrd/error.rb
175
+ - lib/pcrd/migration/orchestrator.rb
176
+ - lib/pcrd/monitor/lag.rb
177
+ - lib/pcrd/options.rb
178
+ - lib/pcrd/output/analyze_printer.rb
179
+ - lib/pcrd/output/cutover_printer.rb
180
+ - lib/pcrd/output/preflight_printer.rb
181
+ - lib/pcrd/output/readiness_printer.rb
182
+ - lib/pcrd/preflight.rb
183
+ - lib/pcrd/readiness/manifest.rb
184
+ - lib/pcrd/replication/consumer.rb
185
+ - lib/pcrd/replication/error.rb
186
+ - lib/pcrd/replication/pgoutput/messages.rb
187
+ - lib/pcrd/replication/pgoutput/parser.rb
188
+ - lib/pcrd/reporter/console.rb
189
+ - lib/pcrd/reporter/null.rb
190
+ - lib/pcrd/schema/column.rb
191
+ - lib/pcrd/schema/ddl.rb
192
+ - lib/pcrd/schema/diff_entry.rb
193
+ - lib/pcrd/schema/differ.rb
194
+ - lib/pcrd/schema/object_reader.rb
195
+ - lib/pcrd/schema/packer.rb
196
+ - lib/pcrd/schema/reader.rb
197
+ - lib/pcrd/schema/setup.rb
198
+ - lib/pcrd/schema/setup_error.rb
199
+ - lib/pcrd/schema/table_not_found.rb
200
+ - lib/pcrd/schema/type_registry.rb
201
+ - lib/pcrd/sql.rb
202
+ - lib/pcrd/transform/row_transformer.rb
203
+ - lib/pcrd/transform/type_map.rb
204
+ - lib/pcrd/transform/validator.rb
205
+ - lib/pcrd/version.rb
206
+ homepage: https://github.com/charlesharris/pcrd
207
+ licenses:
208
+ - MIT
209
+ metadata:
210
+ source_code_uri: https://github.com/charlesharris/pcrd
211
+ changelog_uri: https://github.com/charlesharris/pcrd/blob/main/CHANGELOG.md
212
+ bug_tracker_uri: https://github.com/charlesharris/pcrd/issues
213
+ rubygems_mfa_required: 'true'
214
+ rdoc_options: []
215
+ require_paths:
216
+ - lib
217
+ required_ruby_version: !ruby/object:Gem::Requirement
218
+ requirements:
219
+ - - ">="
220
+ - !ruby/object:Gem::Version
221
+ version: 3.2.0
222
+ required_rubygems_version: !ruby/object:Gem::Requirement
223
+ requirements:
224
+ - - ">="
225
+ - !ruby/object:Gem::Version
226
+ version: '0'
227
+ requirements: []
228
+ rubygems_version: 3.6.7
229
+ specification_version: 4
230
+ summary: PostgreSQL Column Rewrite Daemon — zero-downtime cross-cluster schema migrations
231
+ test_files: []