|
| 1 | +/* |
| 2 | + * Structural signature dump for every object that belongs to an extension |
| 3 | + * (pg_depend deptype = 'e'), used by bin/structural_diff to compare a |
| 4 | + * database reached via an extension UPDATE against a FRESH install of the |
| 5 | + * same target version. Any nonempty diff between two runs of this query is a |
| 6 | + * bug: the two paths are supposed to produce byte-identical objects. |
| 7 | + * |
| 8 | + * Run via: psql -d DBNAME -v extname="'object_reference'" -f bin/structural_diff.sql |
| 9 | + * |
| 10 | + * Modeled on cat_tools's bin/structural_diff.sql (Postgres-Extensions/ |
| 11 | + * cat_tools), which generalized a manual comparison technique (diffing |
| 12 | + * pg_get_functiondef / pg_get_viewdef / type labels / comments / ACLs / |
| 13 | + * extension membership between a fresh install and an updated database) used |
| 14 | + * to find a real fresh-vs-update divergence in that extension. This file is |
| 15 | + * copied near-verbatim -- it is written generically off pg_depend's deptype |
| 16 | + * = 'e' membership edge, with no cat_tools-specific object names, so it |
| 17 | + * applies to object_reference (and any other extension) as-is; only the |
| 18 | + * default :extname in bin/structural_diff's wrapper differs. |
| 19 | + * |
| 20 | + * Emits one text block per member object, ordered by its pg_describe_object() |
| 21 | + * identity so the SAME object sorts to the SAME position regardless of the |
| 22 | + * OIDs assigned along each installation path. Each block covers: |
| 23 | + * - a structural definition, using pg_get_functiondef/pg_get_viewdef for |
| 24 | + * routines/views, an ordered column dump for a plain table or standalone |
| 25 | + * composite type, an ordered label list for enums, or a cast/domain |
| 26 | + * summary -- whichever a member's catalog/kind actually calls for. |
| 27 | + * object_reference currently has no enums, domains or casts of its own; |
| 28 | + * those branches are kept anyway (harmless no-ops today) so a future |
| 29 | + * member of one of those kinds is compared structurally too, without |
| 30 | + * needing to remember to add it. Row types implicitly created BY a |
| 31 | + * member relation, and the array type shadowing any other member type, |
| 32 | + * are skipped: their structure is fully captured by the relation/base- |
| 33 | + * type entry already, so listing them again would just duplicate that |
| 34 | + * comparison under a second identity. |
| 35 | + * - its comment (pg_description), generically via obj_description(). |
| 36 | + * - its ACL, generically via whichever ACL column its catalog has (proacl / |
| 37 | + * typacl / relacl / nspacl); sorted, since grant order is not meaningful. |
| 38 | + * |
| 39 | + * This is deliberately NOT specific to object_reference's current object |
| 40 | + * list: any object kind this extension does not (yet) use falls through to |
| 41 | + * the ELSE branch below, which still includes it (via its |
| 42 | + * pg_describe_object identity, comment and ACL) so a future new member is |
| 43 | + * compared at least at that level rather than silently skipped, even though |
| 44 | + * this file does not (yet) know how to render a structural definition for |
| 45 | + * it. |
| 46 | + */ |
| 47 | +\set ON_ERROR_STOP on |
| 48 | +\pset format unaligned |
| 49 | +\pset tuples_only on |
| 50 | +\pset fieldsep '' |
| 51 | + |
| 52 | +WITH ext AS ( |
| 53 | + SELECT oid FROM pg_extension WHERE extname = :extname |
| 54 | +), members AS ( |
| 55 | + SELECT d.classid, d.objid |
| 56 | + FROM pg_depend d, ext |
| 57 | + WHERE d.refclassid = 'pg_extension'::regclass |
| 58 | + AND d.refobjid = ext.oid |
| 59 | + AND d.deptype = 'e' |
| 60 | +), skip_shadow AS ( |
| 61 | + /* Implicit row type of a member relation: same structure as the relation |
| 62 | + * itself, so comparing it too would just duplicate that check. */ |
| 63 | + SELECT t.oid |
| 64 | + FROM pg_type t |
| 65 | + JOIN members rel ON rel.classid = 'pg_class'::regclass AND rel.objid = t.typrelid |
| 66 | + WHERE t.typtype = 'c' |
| 67 | + UNION |
| 68 | + /* Array type shadowing another member type: same element type, no |
| 69 | + * independent structure of its own. */ |
| 70 | + SELECT t.oid |
| 71 | + FROM pg_type t |
| 72 | + JOIN members base ON base.classid = 'pg_type'::regclass AND base.objid = t.typelem |
| 73 | + WHERE t.typelem <> 0 |
| 74 | +), acl AS ( |
| 75 | + SELECT m.classid, m.objid, |
| 76 | + ( |
| 77 | + SELECT array_to_string(array_agg(a::text ORDER BY a::text), ',') |
| 78 | + FROM unnest( |
| 79 | + CASE m.classid |
| 80 | + WHEN 'pg_proc'::regclass THEN (SELECT proacl FROM pg_proc WHERE oid = m.objid) |
| 81 | + WHEN 'pg_type'::regclass THEN (SELECT typacl FROM pg_type WHERE oid = m.objid) |
| 82 | + WHEN 'pg_class'::regclass THEN (SELECT relacl FROM pg_class WHERE oid = m.objid) |
| 83 | + WHEN 'pg_namespace'::regclass THEN (SELECT nspacl FROM pg_namespace WHERE oid = m.objid) |
| 84 | + ELSE NULL |
| 85 | + END |
| 86 | + ) a |
| 87 | + ) AS acl_text |
| 88 | + FROM members m |
| 89 | +), relation_cols AS ( |
| 90 | + /* Ordered column dump, shared by the plain-table case (pg_class relkind |
| 91 | + * 'r') and the standalone-composite-type case (pg_type typtype 'c' whose |
| 92 | + * typrelid is NOT a member relation, i.e. survived skip_shadow) -- both |
| 93 | + * describe a set of (name, type, not-null, default) columns identically. */ |
| 94 | + SELECT m.classid, m.objid, |
| 95 | + ( |
| 96 | + SELECT string_agg( |
| 97 | + format( |
| 98 | + '%s %s%s%s' |
| 99 | + , a.attname |
| 100 | + , format_type(a.atttypid, a.atttypmod) |
| 101 | + , CASE WHEN a.attnotnull THEN ' NOT NULL' ELSE '' END |
| 102 | + , COALESCE(' DEFAULT ' || pg_get_expr(ad.adbin, ad.adrelid), '') |
| 103 | + ) |
| 104 | + , E'\n' ORDER BY a.attnum |
| 105 | + ) |
| 106 | + FROM pg_attribute a |
| 107 | + LEFT JOIN pg_attrdef ad ON ad.adrelid = a.attrelid AND ad.adnum = a.attnum |
| 108 | + WHERE a.attrelid = CASE m.classid |
| 109 | + WHEN 'pg_class'::regclass THEN m.objid |
| 110 | + WHEN 'pg_type'::regclass THEN (SELECT typrelid FROM pg_type WHERE oid = m.objid) |
| 111 | + END |
| 112 | + AND a.attnum > 0 |
| 113 | + AND NOT a.attisdropped |
| 114 | + ) |
| 115 | + /* Table constraints (PK/UNIQUE/CHECK/FK) have no equivalent on a |
| 116 | + * standalone composite type, so this is NULL there and simply appends |
| 117 | + * nothing. */ |
| 118 | + || COALESCE( |
| 119 | + E'\n' || ( |
| 120 | + SELECT string_agg(pg_get_constraintdef(c.oid), E'\n' ORDER BY c.conname) |
| 121 | + FROM pg_constraint c |
| 122 | + WHERE m.classid = 'pg_class'::regclass AND c.conrelid = m.objid |
| 123 | + ) |
| 124 | + , '' |
| 125 | + ) AS cols |
| 126 | + FROM members m |
| 127 | + WHERE (m.classid = 'pg_class'::regclass AND (SELECT relkind FROM pg_class WHERE oid = m.objid) = 'r') |
| 128 | + OR (m.classid = 'pg_type'::regclass AND (SELECT typtype FROM pg_type WHERE oid = m.objid) = 'c') |
| 129 | +) |
| 130 | +SELECT |
| 131 | + '=== ' || pg_describe_object(m.classid, m.objid, 0) || E' ===\n' |
| 132 | + || 'DEFINITION:' || E'\n' || COALESCE( |
| 133 | + CASE |
| 134 | + WHEN m.classid = 'pg_proc'::regclass |
| 135 | + THEN pg_get_functiondef(m.objid) |
| 136 | + WHEN m.classid = 'pg_class'::regclass AND (SELECT relkind FROM pg_class WHERE oid = m.objid) IN ('v', 'm') |
| 137 | + THEN pg_get_viewdef(m.objid, true) |
| 138 | + WHEN m.classid = 'pg_class'::regclass AND (SELECT relkind FROM pg_class WHERE oid = m.objid) = 'r' |
| 139 | + THEN (SELECT cols FROM relation_cols rc WHERE rc.classid = m.classid AND rc.objid = m.objid) |
| 140 | + WHEN m.classid = 'pg_type'::regclass AND (SELECT typtype FROM pg_type WHERE oid = m.objid) = 'e' |
| 141 | + THEN (SELECT string_agg(enumlabel, ',' ORDER BY enumsortorder) FROM pg_enum WHERE enumtypid = m.objid) |
| 142 | + WHEN m.classid = 'pg_type'::regclass AND (SELECT typtype FROM pg_type WHERE oid = m.objid) = 'c' |
| 143 | + THEN (SELECT cols FROM relation_cols rc WHERE rc.classid = m.classid AND rc.objid = m.objid) |
| 144 | + WHEN m.classid = 'pg_type'::regclass AND (SELECT typtype FROM pg_type WHERE oid = m.objid) = 'd' |
| 145 | + THEN ( |
| 146 | + SELECT format( |
| 147 | + 'base=%s notnull=%s default=%s check=%s' |
| 148 | + , t.typbasetype::regtype, t.typnotnull, t.typdefault |
| 149 | + , (SELECT string_agg(pg_get_constraintdef(c.oid), ' AND ' ORDER BY c.oid) |
| 150 | + FROM pg_constraint c WHERE c.contypid = m.objid) |
| 151 | + ) |
| 152 | + FROM pg_type t WHERE t.oid = m.objid |
| 153 | + ) |
| 154 | + WHEN m.classid = 'pg_cast'::regclass |
| 155 | + THEN ( |
| 156 | + SELECT format( |
| 157 | + 'CAST (%s AS %s) METHOD %s CONTEXT %s' |
| 158 | + , ct.castsource::regtype, ct.casttarget::regtype |
| 159 | + , CASE ct.castmethod |
| 160 | + WHEN 'f' THEN 'FUNCTION ' || ct.castfunc::regprocedure::text |
| 161 | + WHEN 'i' THEN 'INOUT' |
| 162 | + WHEN 'b' THEN 'BINARY COERCION' |
| 163 | + END |
| 164 | + , ct.castcontext |
| 165 | + ) |
| 166 | + FROM pg_cast ct WHERE ct.oid = m.objid |
| 167 | + ) |
| 168 | + ELSE NULL |
| 169 | + END |
| 170 | + , '(no structural definition rendered for this object kind -- see identity/comment/ACL below)' |
| 171 | + ) |
| 172 | + || E'\n' || 'COMMENT: ' || COALESCE(obj_description(m.objid, m.classid::regclass::text), '(none)') |
| 173 | + || E'\n' || 'ACL: ' || COALESCE((SELECT acl_text FROM acl WHERE acl.classid = m.classid AND acl.objid = m.objid), '(none)') |
| 174 | + || E'\n' |
| 175 | + AS block |
| 176 | + FROM members m |
| 177 | + LEFT JOIN skip_shadow s ON m.classid = 'pg_type'::regclass AND s.oid = m.objid |
| 178 | + WHERE s.oid IS NULL |
| 179 | + ORDER BY pg_describe_object(m.classid, m.objid, 0); |
0 commit comments