From 61e8338269fe3e90ff6af0026872a22cc8246188 Mon Sep 17 00:00:00 2001 From: "shipmill-romamo[bot]" <338557544+shipmill-romamo[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2026 17:08:17 +0200 Subject: [PATCH] Name an extension used through an array or a domain in the data copy preflight (S-007-7) A citext[] column depends on the array type _citext, which depends on citext with deptype 'i', not on the extension, so the preflight's needed CTE never reached the extension member: citext was neither reported under extensions nor passed to pg_dump --extension, and the restore failed. The query now walks every type a selected object references to its array element type (typcategory 'A', typelem) and its domain base type (typbasetype), chained, before matching extension members, in the one psql-variable-parameterised query that feeds both lists. Fixes #173 Co-Authored-By: Claude Opus 5.5 --- changelog.d/173-preflight-extension-arrays.md | 3 ++ .../defaults/main.yml | 28 +++++++++++++++---- engine/tests/test_data_migration_role.py | 17 +++++++++++ 3 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 changelog.d/173-preflight-extension-arrays.md diff --git a/changelog.d/173-preflight-extension-arrays.md b/changelog.d/173-preflight-extension-arrays.md new file mode 100644 index 0000000..edcde41 --- /dev/null +++ b/changelog.d/173-preflight-extension-arrays.md @@ -0,0 +1,3 @@ +### Fixed + +- The data copy preflight now names an extension that a selected table needs through an array or a domain of the extension's type, such as a `citext[]` column or a column of a domain over `citext` (chained domains included), under `extensions` and passes it to `pg_dump --extension`; before, only a direct use of the extension's type was seen, so the copy skipped the extension and the restore failed (#173) diff --git a/engine/ansible/roles/cloudfall_data_migration/defaults/main.yml b/engine/ansible/roles/cloudfall_data_migration/defaults/main.yml index 1b84604..3ecdc11 100644 --- a/engine/ansible/roles/cloudfall_data_migration/defaults/main.yml +++ b/engine/ansible/roles/cloudfall_data_migration/defaults/main.yml @@ -27,15 +27,18 @@ cloudfall_data_migration_target_tables_query: >- # extensions: an extension the selected schemas need: one installed in a # selected schema, one whose member object an object of a selected schema # depends on in pg_depend (a column type, a default, an index operator -# class, a view, a trigger, a policy), or, with no schema named, every -# extension but plpgsql, since the whole dump creates them all +# class, a view, a trigger, a policy), following an array type to its +# element type and a domain to its base type, chained, since a citext[] +# column depends on _citext and only citext is the extension's member, +# or, with no schema named, every extension but plpgsql, since the whole +# dump creates them all # policyRoles: a role a row-level security policy on a table of a selected # schema names (pg_policy.polroles; 0 is PUBLIC) # authReferences: when auth isn't selected, a function of a selected schema # whose source mentions auth. or that depends on an object in auth, and a # policy on a table of a selected schema whose USING or WITH CHECK does cloudfall_data_migration_source_preflight_query: >- - WITH selected AS ( + WITH RECURSIVE selected AS ( SELECT jsonb_array_elements_text(:'cloudfall_data_schemas'::jsonb) AS nspname ), @@ -83,15 +86,28 @@ cloudfall_data_migration_source_preflight_query: >- WHERE d.refclassid = 'pg_catalog.pg_extension'::regclass AND d.deptype = 'e' ), + referenced(classid, objid) AS ( + SELECT d.refclassid, d.refobjid + FROM pg_catalog.pg_depend d + JOIN selected_objects o ON o.classid = d.classid AND o.objid = d.objid + UNION + SELECT 'pg_catalog.pg_type'::regclass::oid, under.oid + FROM referenced r JOIN pg_catalog.pg_type t + ON r.classid = 'pg_catalog.pg_type'::regclass AND t.oid = r.objid + CROSS JOIN LATERAL (VALUES + (CASE WHEN t.typcategory = 'A' THEN t.typelem END), + (CASE WHEN t.typtype = 'd' THEN t.typbasetype END) + ) AS under(oid) + WHERE under.oid <> 0 + ), needed AS ( SELECT e.extname FROM pg_catalog.pg_extension e WHERE (:'cloudfall_data_every_schema'::boolean AND e.extname <> 'plpgsql') OR e.extnamespace IN (SELECT oid FROM selected_ns) OR e.oid IN ( SELECT m.extoid - FROM pg_catalog.pg_depend d - JOIN selected_objects o ON o.classid = d.classid AND o.objid = d.objid - JOIN members m ON m.classid = d.refclassid AND m.objid = d.refobjid + FROM referenced r JOIN members m + ON m.classid = r.classid AND m.objid = r.objid UNION SELECT d.refobjid FROM pg_catalog.pg_depend d diff --git a/engine/tests/test_data_migration_role.py b/engine/tests/test_data_migration_role.py index 40613f7..ab73e65 100644 --- a/engine/tests/test_data_migration_role.py +++ b/engine/tests/test_data_migration_role.py @@ -472,6 +472,23 @@ def test_s007_7_an_extension_the_target_lacks_is_refused() -> None: assert "e.extname <> 'plpgsql'" in sql +def test_s007_7_an_array_or_domain_of_an_extension_type_names_it() -> None: + sql = _source_preflight_sql(["public"], every_schema=False) + needed = sql[sql.index("needed AS (") : sql.index("missing AS (")] + + # A citext[] column depends on _citext, which depends on citext with + # deptype 'i', not on the extension: the walk follows an array to its + # element type and a domain to its base type, chained, before it + # matches extension members, in the one query that feeds both the + # extensions finding and pg_dump --extension. + assert sql.startswith("WITH RECURSIVE selected AS (") + assert "referenced(classid, objid) AS (" in sql + assert "FROM referenced r JOIN pg_catalog.pg_type t" in sql + assert "WHEN t.typcategory = 'A' THEN t.typelem" in sql + assert "WHEN t.typtype = 'd' THEN t.typbasetype" in sql + assert "FROM referenced r JOIN members m" in needed + + def test_s007_7_named_schemas_dump_their_needed_extensions() -> None: shell = _module( "Dump the source database on the target host", "ansible.builtin.shell"