exwiw 0.9.19 → 0.9.20
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 +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +7 -0
- data/lib/exwiw/adapter/postgresql_adapter.rb +79 -1
- data/lib/exwiw/ddl_postprocessor.rb +44 -0
- data/lib/exwiw/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 328fbb843d5c15da7ea9f764adda96a5c58b4d24de615b9cac38ba305550ed05
|
|
4
|
+
data.tar.gz: 691448e490f27475d1ae17be240f89af5ea7c3ac54ddedfa544ca1b71517f5c1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ee57c7925b2b912a3e9777947d251b8870f94572ec96b5d7fbd0a8d60dbb8aaefd33cde4e12e735b2ff2eb44b24e2f101b81e4bdfc78752f55788619a4ce5a6c
|
|
7
|
+
data.tar.gz: 9f3a2b95adb72322032b7ac4967d69e3c32a0df6fa75c64adc7f59be1f9de6b7958a8250c0abb2fd9857a5c063d8123f71c2d9678376757d1161ef34f484f368
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.9.20] - 2026-08-05
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- **PostgreSQL: the extensions a managed platform installs to run the source instance itself (Cloud SQL / AlloyDB) are now explicitly out of target and left out of `insert-000-schema.sql`.** A full-database `pg_dump` of a Cloud SQL instance emits `CREATE SCHEMA google_vacuum_mgmt` plus the `CREATE EXTENSION` / `COMMENT ON EXTENSION` for it; an AlloyDB one emits `google_columnar_engine` and `google_db_advisor`. exwiw already made these survivable — earlier releases wrapped both statements in `DO` blocks that warn-and-skip when the restore target cannot provide the extension — but they still occupied the dump, left an empty vendor schema behind on every restore, and logged a `WARNING` each time. They serve the *source* instance's operation (vacuum tuning, the in-memory columnar cache, index advice), carry no application data, are named by nothing in the application's own schema, and ship only with the managed platform, so no restore target outside it can ever create them. Both halves are now dropped: their schemas via `pg_dump --exclude-schema` (which also covers any object the platform adds inside one later), and the extension statements — not schema-qualified, so no `pg_dump` filter reaches them — via a new `DdlPostprocessor.strip_extensions` pass, with whatever was excluded named in the run's log rather than dropped silently. The set is an explicit list of names (`PostgresqlAdapter::PLATFORM_MANAGED_EXTENSIONS`), deliberately not a `google_*` prefix match: the prefixes are not reserved, so a prefix rule would also drop a schema an application legitimately owns — `google_calendar` for a Google Calendar integration — together with its tables. Adding a name costs a release, which is the right trade. Every other extension is still emitted wrapped in the warn-and-skip `DO` block, including two kinds that are also managed-platform-only: a third-party extension pulled in as a dependency of an excluded one (`google_db_advisor` requires `hypopg`, installable on any plain PostgreSQL), and an application-facing platform extension (`google_ml_integration`, `alloydb_scann`, `alloydb_ai_nl`), which application SQL and dumped DDL can name (a ScaNN index is `USING scann`) — removing its `CREATE` would strand whatever refers to it. Data extraction never touched any of these objects — it is driven by the schema config, which has no entry for a table without an application model — so no `insert-*` / `delete-*` file changes.
|
|
10
|
+
|
|
5
11
|
## [0.9.19] - 2026-08-04
|
|
6
12
|
|
|
7
13
|
### Added
|
data/README.md
CHANGED
|
@@ -108,6 +108,13 @@ so you should import the dump in order.
|
|
|
108
108
|
|
|
109
109
|
`insert-000-schema.sql` is generated by shelling out to the database client tools (`mysqldump` for `mysql`, `pg_dump` for `postgresql`, and the sqlite3 driver for `sqlite`), so the corresponding client must be available on PATH when running exwiw. For `mysql`, set `EXWIW_MYSQLDUMP` to point at a specific `mysqldump` binary when the one on PATH is incompatible with the server (e.g. a MySQL 9.x `mysqldump` cannot load `mysql_native_password` against a server still using that auth plugin — `EXWIW_MYSQLDUMP=/path/to/mysql@8.0/bin/mysqldump`). The output is post-processed to make it idempotent: `CREATE TABLE IF NOT EXISTS`, `CREATE INDEX IF NOT EXISTS` (where the engine supports it), and PostgreSQL's `ALTER TABLE ... ADD CONSTRAINT` statements are wrapped in `DO $$ ... EXCEPTION WHEN duplicate_object`. For `mysql`, the source server's `DEFINER=user@host` stamp on views and triggers is stripped too, so restoring into a managed MySQL instance (which usually can't grant the privilege to recreate someone else's `DEFINER`) does not fail.
|
|
110
110
|
|
|
111
|
+
For `postgresql`, the extensions a managed platform installs to run the source instance itself are treated as out of target and left out of the dump entirely — currently `google_vacuum_mgmt` (Cloud SQL / AlloyDB adaptive autovacuum), `google_columnar_engine` and `google_db_advisor` (AlloyDB). They serve the source instance's operation (vacuum tuning, the in-memory columnar cache, index advice), hold no application data, are referenced by nothing in the application's own schema, and ship only with the managed platform, so a restore target outside it can never create them. Their schemas are dropped via `pg_dump --exclude-schema` and their `CREATE EXTENSION` / `COMMENT ON EXTENSION` statements — which are not schema-qualified, so no `pg_dump` filter reaches them — are removed from the output; whatever was excluded is named in the run's log.
|
|
112
|
+
|
|
113
|
+
The list is exact names, not a `google_*` prefix match: those prefixes are not reserved, so a prefix rule would also drop a schema an application legitimately owns (`google_calendar` for a Google Calendar integration) together with its tables. Every other extension is kept and wrapped in the usual warn-and-skip `DO` block, including two kinds that are also managed-platform-only:
|
|
114
|
+
|
|
115
|
+
- a third-party extension pulled in as a dependency of an excluded one (`google_db_advisor` requires `hypopg`), since that one *is* installable on a plain PostgreSQL, and
|
|
116
|
+
- an application-facing platform extension (`google_ml_integration`, `alloydb_scann`, `alloydb_ai_nl`), which the application's own SQL and DDL can name (a ScaNN index is `USING scann`) — removing its `CREATE` would strand whatever refers to it, so it warns and skips instead.
|
|
117
|
+
|
|
111
118
|
you need to delete the records before importing the dump,
|
|
112
119
|
`delete-{idx}-{table_name}.sql` will help you to do that.
|
|
113
120
|
This sql will delete "all" related records to the extract targets.
|
|
@@ -98,6 +98,41 @@ module Exwiw
|
|
|
98
98
|
connection.exec("EXPLAIN #{sql}").values.map(&:first).join("\n")
|
|
99
99
|
end
|
|
100
100
|
|
|
101
|
+
# Extensions a managed PostgreSQL platform installs to run the *source*
|
|
102
|
+
# instance itself, which exwiw treats as out of target and leaves out of the
|
|
103
|
+
# dump entirely. Each one here satisfies both conditions:
|
|
104
|
+
#
|
|
105
|
+
# 1. it ships only with the managed platform, so no restore target outside
|
|
106
|
+
# that platform can create it, and
|
|
107
|
+
# 2. nothing in the application's own schema or queries references it — it
|
|
108
|
+
# is operational machinery (autovacuum tuning, the in-memory columnar
|
|
109
|
+
# cache, index advice), not a feature the app builds on.
|
|
110
|
+
#
|
|
111
|
+
# Condition 2 is what keeps this list short. AlloyDB's application-facing
|
|
112
|
+
# extensions — `google_ml_integration`, `alloydb_scann`, `alloydb_ai_nl` —
|
|
113
|
+
# meet condition 1 but are called from application SQL and can be named by
|
|
114
|
+
# dumped DDL (a ScaNN index is `USING scann`), so dropping their CREATE would
|
|
115
|
+
# silently strand whatever refers to them. They stay in the dump, wrapped in
|
|
116
|
+
# the usual warn-and-skip DO block, like `pglogical` and like a third-party
|
|
117
|
+
# extension pulled in as a dependency of one listed here (`google_db_advisor`
|
|
118
|
+
# requires `hypopg`, which any plain PostgreSQL can install).
|
|
119
|
+
#
|
|
120
|
+
# Deliberately an explicit list of names, not a `google_`/`alloydb_` prefix
|
|
121
|
+
# match (which is how the old `pg_extension` query filtered, before switching
|
|
122
|
+
# to a full-database pg_dump dropped that query and the filter with it): the
|
|
123
|
+
# prefixes are not reserved, so a schema an application legitimately owns —
|
|
124
|
+
# `google_calendar` for a Google Calendar integration — would be matched and
|
|
125
|
+
# dropped together with its tables. The vendor gains extensions faster than
|
|
126
|
+
# this list does; that costs a release, which is the right trade against
|
|
127
|
+
# deleting an application's own objects. Its `rds_%` / `aiven_%` arms are not
|
|
128
|
+
# carried over for the same reason: naming their members takes a dump to
|
|
129
|
+
# confirm, and adding one here is a one-line change.
|
|
130
|
+
PLATFORM_MANAGED_EXTENSIONS = %w[
|
|
131
|
+
google_columnar_engine
|
|
132
|
+
google_db_advisor
|
|
133
|
+
google_vacuum_mgmt
|
|
134
|
+
].freeze
|
|
135
|
+
|
|
101
136
|
def dump_schema(ordered_tables, output_path)
|
|
102
137
|
require 'open3'
|
|
103
138
|
|
|
@@ -117,10 +152,20 @@ module Exwiw
|
|
|
117
152
|
'--schema-only',
|
|
118
153
|
'--no-owner',
|
|
119
154
|
'--no-acl',
|
|
155
|
+
# An extension that needs a schema of its own puts it under its own name
|
|
156
|
+
# (Cloud SQL's google_vacuum_mgmt does; the AlloyDB two install into
|
|
157
|
+
# public), so excluding the same names covers the schema half — including
|
|
158
|
+
# any object the platform adds inside one later. The patterns are exact
|
|
159
|
+
# names: pg_dump reads them psql \d-style, where `_` is literal and only
|
|
160
|
+
# `*` globs. A pattern matching nothing is not an error (that needs
|
|
161
|
+
# --strict-names), so these are harmless against a self-hosted server.
|
|
162
|
+
*PLATFORM_MANAGED_EXTENSIONS.map { |name| "--exclude-schema=#{name}" },
|
|
120
163
|
@connection_config.database_name,
|
|
121
164
|
]
|
|
122
165
|
env = { 'PGPASSWORD' => @connection_config.password.to_s }
|
|
123
166
|
|
|
167
|
+
log_excluded_platform_managed_schemas
|
|
168
|
+
|
|
124
169
|
@logger.debug(" Running pg_dump for the whole database (#{@connection_config.database_name})...")
|
|
125
170
|
stdout, stderr, status = Open3.capture3(env, *cmd)
|
|
126
171
|
unless status.success?
|
|
@@ -141,7 +186,9 @@ module Exwiw
|
|
|
141
186
|
# EXTENSION that pg_dump emits alongside is likewise wrapped to swallow
|
|
142
187
|
# undefined_object, so a skipped extension's trailing comment does not
|
|
143
188
|
# abort the restore either.
|
|
144
|
-
|
|
189
|
+
# Platform-managed extensions are stripped first: the wrapping passes below
|
|
190
|
+
# rewrite the bare CREATE/COMMENT statements this removes.
|
|
191
|
+
idempotent = strip_platform_managed_extensions(stdout)
|
|
145
192
|
idempotent = DdlPostprocessor.wrap_create_type_enum_in_do_block(idempotent)
|
|
146
193
|
idempotent = DdlPostprocessor.wrap_create_extension_in_do_block(idempotent)
|
|
147
194
|
idempotent = DdlPostprocessor.wrap_comment_on_extension_in_do_block(idempotent)
|
|
@@ -159,6 +206,37 @@ module Exwiw
|
|
|
159
206
|
@logger.info(" Wrote full-database schema to #{output_path} (#{ordered_tables.size} table(s) in scope for data).")
|
|
160
207
|
end
|
|
161
208
|
|
|
209
|
+
# Drop the platform-managed extensions from a raw pg_dump, naming the ones
|
|
210
|
+
# actually removed so the run records what it left out rather than silently
|
|
211
|
+
# dropping it. See PLATFORM_MANAGED_EXTENSIONS.
|
|
212
|
+
private def strip_platform_managed_extensions(sql)
|
|
213
|
+
removed = DdlPostprocessor.extension_names(sql) & PLATFORM_MANAGED_EXTENSIONS
|
|
214
|
+
unless removed.empty?
|
|
215
|
+
@logger.info(" Excluded platform-managed extension(s) from the schema dump: #{removed.join(', ')}.")
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
DdlPostprocessor.strip_extensions(sql, PLATFORM_MANAGED_EXTENSIONS)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Name the --exclude-schema patterns the source instance actually has, so the
|
|
222
|
+
# run records the schema half of the exclusion the way
|
|
223
|
+
# #strip_platform_managed_extensions records the extension half. Only exact
|
|
224
|
+
# names are excluded, but nothing stops an application from owning one of
|
|
225
|
+
# them, and excluding a schema takes every object inside it along — that must
|
|
226
|
+
# not happen without the log saying so. See PLATFORM_MANAGED_EXTENSIONS.
|
|
227
|
+
private def log_excluded_platform_managed_schemas
|
|
228
|
+
# The names are bare identifiers from the constant, so they need no quoting
|
|
229
|
+
# inside the array literal ANY() takes.
|
|
230
|
+
result = connection.exec_params(
|
|
231
|
+
'SELECT nspname FROM pg_namespace WHERE nspname = ANY($1) ORDER BY nspname',
|
|
232
|
+
["{#{PLATFORM_MANAGED_EXTENSIONS.join(',')}}"],
|
|
233
|
+
)
|
|
234
|
+
excluded = result.values.map(&:first)
|
|
235
|
+
return if excluded.empty?
|
|
236
|
+
|
|
237
|
+
@logger.info(" Excluded platform-managed schema(s) from the schema dump: #{excluded.join(', ')}.")
|
|
238
|
+
end
|
|
239
|
+
|
|
162
240
|
# The INSERT header for this adapter. PostgreSQL uses bare identifiers,
|
|
163
241
|
# quoted only when required (reserved word / unsafe characters).
|
|
164
242
|
# #to_bulk_insert / #write_inserts (SqlBulkInsert) append the value tuples
|
|
@@ -170,6 +170,50 @@ module Exwiw
|
|
|
170
170
|
end
|
|
171
171
|
end
|
|
172
172
|
|
|
173
|
+
# Every extension name a dump installs, in the order the CREATE EXTENSION
|
|
174
|
+
# statements appear. Used to report which of them #strip_extensions is about
|
|
175
|
+
# to drop; run it on the raw dump, before any wrapping pass.
|
|
176
|
+
def extension_names(sql)
|
|
177
|
+
sql.scan(CREATE_EXTENSION_RE).map { |(name)| name.delete('"') }
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Drop every trace of the extensions named in `names` — the ones a managed
|
|
181
|
+
# PostgreSQL platform installs to run the source instance itself (see
|
|
182
|
+
# PostgresqlAdapter::PLATFORM_MANAGED_EXTENSIONS). Removed rather than merely
|
|
183
|
+
# wrapped like any other extension, because they cannot be created anywhere
|
|
184
|
+
# outside that platform, so keeping them only leaves a restore-time WARNING
|
|
185
|
+
# and objects the target will never have.
|
|
186
|
+
#
|
|
187
|
+
# Three things are emitted per extension and all three must go, or a leftover
|
|
188
|
+
# references a name that is no longer installed: pg_dump's `-- Name: <ext>;
|
|
189
|
+
# Type: EXTENSION` header block, the `CREATE EXTENSION`, and the
|
|
190
|
+
# `COMMENT ON EXTENSION` (whose own header reads `-- Name: EXTENSION <ext>;
|
|
191
|
+
# Type: COMMENT`). Run this on the raw dump, before the wrapping passes: they
|
|
192
|
+
# rewrite the bare statements this matches into DO blocks.
|
|
193
|
+
def strip_extensions(sql, names)
|
|
194
|
+
return sql if names.empty?
|
|
195
|
+
|
|
196
|
+
# Whole names only: each match is anchored by the whitespace/quote before it
|
|
197
|
+
# and a \b after, so `google_vacuum_mgmt` never matches an extension merely
|
|
198
|
+
# containing it (`not_google_vacuum_mgmt`, `google_vacuum_mgmt_v2`).
|
|
199
|
+
names_re = Regexp.union(names.map { |n| Regexp.escape(n) })
|
|
200
|
+
name = /(?:"#{names_re}"|#{names_re}\b)/
|
|
201
|
+
|
|
202
|
+
# Each removal takes the blank lines that trailed the object too, so the
|
|
203
|
+
# surrounding statements keep pg_dump's spacing instead of gaining a gap.
|
|
204
|
+
trailing_blank_lines = /(?:[ \t]*\r?\n(?:[ \t]*\r?\n)*)?/
|
|
205
|
+
|
|
206
|
+
sql = sql.gsub(
|
|
207
|
+
/^--\r?\n-- Name: (?:EXTENSION\s+)?#{name};[ \t]*Type:[ \t]*(?:EXTENSION|COMMENT);[^\n]*\n--(?:\r?\n)+/i,
|
|
208
|
+
"",
|
|
209
|
+
)
|
|
210
|
+
sql = sql.gsub(/^[ \t]*CREATE\s+EXTENSION\b(?:\s+IF\s+NOT\s+EXISTS)?\s+#{name}[^;]*;#{trailing_blank_lines}/i, "")
|
|
211
|
+
sql.gsub(
|
|
212
|
+
/^[ \t]*COMMENT\s+ON\s+EXTENSION\s+#{name}\s+IS\s+(?:'(?:[^']|'')*'|NULL)\s*;#{trailing_blank_lines}/i,
|
|
213
|
+
"",
|
|
214
|
+
)
|
|
215
|
+
end
|
|
216
|
+
|
|
173
217
|
# Generate idempotent CREATE TYPE ... AS ENUM statements.
|
|
174
218
|
# +enum_types+ is an Array of Hashes with keys :schema, :name, :labels.
|
|
175
219
|
def create_type_enum_statements(enum_types)
|
data/lib/exwiw/version.rb
CHANGED