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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +24 -0
- data/LICENSE +21 -0
- data/README.md +614 -0
- data/bin/pcrd +7 -0
- data/lib/pcrd/advisory_lock.rb +50 -0
- data/lib/pcrd/apply/engine.rb +184 -0
- data/lib/pcrd/apply/worker.rb +97 -0
- data/lib/pcrd/backfill/batch.rb +158 -0
- data/lib/pcrd/backfill/engine.rb +153 -0
- data/lib/pcrd/checkpoint/store.rb +217 -0
- data/lib/pcrd/cli.rb +274 -0
- data/lib/pcrd/commands/analyze.rb +125 -0
- data/lib/pcrd/commands/cleanup.rb +112 -0
- data/lib/pcrd/commands/demo.rb +152 -0
- data/lib/pcrd/commands/readiness.rb +30 -0
- data/lib/pcrd/commands/status.rb +129 -0
- data/lib/pcrd/commands/verify.rb +172 -0
- data/lib/pcrd/config/add_column.rb +7 -0
- data/lib/pcrd/config/analyze_config.rb +8 -0
- data/lib/pcrd/config/column_spec.rb +10 -0
- data/lib/pcrd/config/connection.rb +7 -0
- data/lib/pcrd/config/cutover_config.rb +7 -0
- data/lib/pcrd/config/load_error.rb +7 -0
- data/lib/pcrd/config/loader.rb +158 -0
- data/lib/pcrd/config/migrate_config.rb +21 -0
- data/lib/pcrd/config/root.rb +9 -0
- data/lib/pcrd/config/schema.rb +62 -0
- data/lib/pcrd/config/table.rb +9 -0
- data/lib/pcrd/config/verify_config.rb +7 -0
- data/lib/pcrd/config.rb +7 -0
- data/lib/pcrd/connection/client.rb +129 -0
- data/lib/pcrd/connection/error.rb +7 -0
- data/lib/pcrd/connection/replication.rb +108 -0
- data/lib/pcrd/cutover/orchestrator.rb +108 -0
- data/lib/pcrd/cutover/sequences.rb +138 -0
- data/lib/pcrd/demo/generator.rb +214 -0
- data/lib/pcrd/demo/schema.rb +154 -0
- data/lib/pcrd/error.rb +12 -0
- data/lib/pcrd/migration/orchestrator.rb +272 -0
- data/lib/pcrd/monitor/lag.rb +107 -0
- data/lib/pcrd/options.rb +15 -0
- data/lib/pcrd/output/analyze_printer.rb +173 -0
- data/lib/pcrd/output/cutover_printer.rb +128 -0
- data/lib/pcrd/output/preflight_printer.rb +119 -0
- data/lib/pcrd/output/readiness_printer.rb +72 -0
- data/lib/pcrd/preflight.rb +331 -0
- data/lib/pcrd/readiness/manifest.rb +201 -0
- data/lib/pcrd/replication/consumer.rb +235 -0
- data/lib/pcrd/replication/error.rb +10 -0
- data/lib/pcrd/replication/pgoutput/messages.rb +68 -0
- data/lib/pcrd/replication/pgoutput/parser.rb +316 -0
- data/lib/pcrd/reporter/console.rb +46 -0
- data/lib/pcrd/reporter/null.rb +14 -0
- data/lib/pcrd/schema/column.rb +59 -0
- data/lib/pcrd/schema/ddl.rb +71 -0
- data/lib/pcrd/schema/diff_entry.rb +36 -0
- data/lib/pcrd/schema/differ.rb +175 -0
- data/lib/pcrd/schema/object_reader.rb +187 -0
- data/lib/pcrd/schema/packer.rb +90 -0
- data/lib/pcrd/schema/reader.rb +118 -0
- data/lib/pcrd/schema/setup.rb +143 -0
- data/lib/pcrd/schema/setup_error.rb +9 -0
- data/lib/pcrd/schema/table_not_found.rb +8 -0
- data/lib/pcrd/schema/type_registry.rb +116 -0
- data/lib/pcrd/sql.rb +55 -0
- data/lib/pcrd/transform/row_transformer.rb +69 -0
- data/lib/pcrd/transform/type_map.rb +209 -0
- data/lib/pcrd/transform/validator.rb +106 -0
- data/lib/pcrd/version.rb +5 -0
- data/lib/pcrd.rb +11 -0
- 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
|
data/lib/pcrd/version.rb
ADDED
data/lib/pcrd.rb
ADDED
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: []
|