]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
Add paths of extensions to pg_available_extensions
authorAndrew Dunstan <andrew@dunslane.net>
Thu, 1 Jan 2026 17:11:37 +0000 (12:11 -0500)
committerAndrew Dunstan <andrew@dunslane.net>
Thu, 1 Jan 2026 17:13:59 +0000 (12:13 -0500)
Add a new "location" column to the pg_available_extensions and
pg_available_extension_versions views, exposing the directory where
the extension is located.

The default system location is shown as '$system', the same value
that can be used to configure the extension_control_path GUC.

User-defined locations are only visible for super users, otherwise
'<insufficient privilege>' is returned as a column value, the same
behaviour that we already use in pg_stat_activity.

I failed to resist the temptation to do a little extra editorializing of
the TAP test script.

Catalog version bumped.

Author: Matheus Alcantara <mths.dev@pm.me>
Reviewed-By: Chao Li <li.evan.chao@gmail.com>
Reviewed-By: Rohit Prasad <rohit.prasad@arm.com>
Reviewed-By: Michael Banck <mbanck@gmx.net>
Reviewed-By: Manni Wood <manni.wood@enterprisedb.com>
Reviewed-By: Euler Taveira <euler@eulerto.com>
Reviewed-By: Quan Zongliang <quanzongliang@yeah.net>
doc/src/sgml/system-views.sgml
src/backend/catalog/system_views.sql
src/backend/commands/extension.c
src/include/catalog/catversion.h
src/include/catalog/pg_proc.dat
src/test/modules/test_extensions/t/001_extension_control_path.pl
src/test/regress/expected/rules.out
src/tools/pgindent/typedefs.list

index 7ff7ca4f7193d5e46eaf6007423dab623f7f599b..8b4abef8c681ece5e2d218f8ca91b9a2daaf0fac 100644 (file)
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>location</structfield> <type>text</type>
+      </para>
+      <para>
+       The location where the extension is installed. If it is in the standard
+       system location, then the value will be <literal>$system</literal>,
+       while if it is found in the path specified by the
+       <link linkend="guc-extension-control-path"><structname>extension_control_path</structname></link>
+       GUC then the full path will be shown.
+       Only superusers can see this information.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>comment</structfield> <type>text</type>
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>location</structfield> <type>text</type>
+      </para>
+       <para>
+       The location where the extension is installed. If it is in the standard
+       system location, then the value will be <literal>$system</literal>,
+       while if it is found in the path specified by the
+       <link linkend="guc-extension-control-path"><structname>extension_control_path</structname></link>
+       GUC then the full path will be shown.
+       Only superusers can see this information.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>comment</structfield> <type>text</type>
index 0a0f95f6bb9fe62d87e395b2df1d73f6e1745e3c..a173779df3229b80aadd21bd324d65e91104eac1 100644 (file)
@@ -412,14 +412,14 @@ CREATE VIEW pg_cursors AS
 
 CREATE VIEW pg_available_extensions AS
     SELECT E.name, E.default_version, X.extversion AS installed_version,
-           E.comment
+           E.location, E.comment
       FROM pg_available_extensions() AS E
            LEFT JOIN pg_extension AS X ON E.name = X.extname;
 
 CREATE VIEW pg_available_extension_versions AS
     SELECT E.name, E.version, (X.extname IS NOT NULL) AS installed,
            E.superuser, E.trusted, E.relocatable,
-           E.schema, E.requires, E.comment
+           E.schema, E.requires, E.location, E.comment
       FROM pg_available_extension_versions() AS E
            LEFT JOIN pg_extension AS X
              ON E.name = X.extname AND E.version = X.extversion;
index c43b74e319e809983caadee9eb40048b709aca58..fc33e5569deeaa1831ca4ba4b7e3653e1ae4e75a 100644 (file)
@@ -126,6 +126,21 @@ typedef struct
        ParseLoc        stmt_len;               /* length in bytes; 0 means "rest of string" */
 } script_error_callback_arg;
 
+/*
+ * A location based on the extension_control_path GUC.
+ *
+ * The macro field stores the name of a macro (for example “$system”) that
+ * the extension_control_path processing supports, and which can be replaced
+ * by a system value stored in loc.
+ *
+ * For non-system paths the macro field is NULL.
+ */
+typedef struct
+{
+       char       *macro;
+       char       *loc;
+} ExtensionLocation;
+
 /* Local functions */
 static List *find_update_path(List *evi_list,
                                                          ExtensionVersionInfo *evi_start,
@@ -140,7 +155,8 @@ static Oid  get_required_extension(char *reqExtensionName,
                                                                   bool is_create);
 static void get_available_versions_for_extension(ExtensionControlFile *pcontrol,
                                                                                                 Tuplestorestate *tupstore,
-                                                                                                TupleDesc tupdesc);
+                                                                                                TupleDesc tupdesc,
+                                                                                                ExtensionLocation *location);
 static Datum convert_requires_to_datum(List *requires);
 static void ApplyExtensionUpdates(Oid extensionOid,
                                                                  ExtensionControlFile *pcontrol,
@@ -157,6 +173,29 @@ static ExtensionControlFile *new_ExtensionControlFile(const char *extname);
 
 char      *find_in_paths(const char *basename, List *paths);
 
+/*
+ *  Return the extension location. If the current user doesn't have sufficient
+ *  privilege, don't show the location.
+ */
+static char *
+get_extension_location(ExtensionLocation *loc)
+{
+       /* We only want to show extension paths for superusers. */
+       if (superuser())
+       {
+               /* Return the macro value if present to avoid showing system paths. */
+               if (loc->macro != NULL)
+                       return loc->macro;
+               else
+                       return loc->loc;
+       }
+       else
+       {
+               /* Similar to pg_stat_activity for unprivileged users */
+               return "<insufficient privilege>";
+       }
+}
+
 /*
  * get_extension_oid - given an extension name, look up the OID
  *
@@ -354,7 +393,11 @@ get_extension_control_directories(void)
 
        if (strlen(Extension_control_path) == 0)
        {
-               paths = lappend(paths, system_dir);
+               ExtensionLocation *location = palloc_object(ExtensionLocation);
+
+               location->macro = NULL;
+               location->loc = system_dir;
+               paths = lappend(paths, location);
        }
        else
        {
@@ -366,6 +409,7 @@ get_extension_control_directories(void)
                        int                     len;
                        char       *mangled;
                        char       *piece = first_path_var_separator(ecp);
+                       ExtensionLocation *location = palloc_object(ExtensionLocation);
 
                        /* Get the length of the next path on ecp */
                        if (piece == NULL)
@@ -382,15 +426,21 @@ get_extension_control_directories(void)
                         * suffix if it is a custom extension control path.
                         */
                        if (strcmp(piece, "$system") == 0)
+                       {
+                               location->macro = pstrdup(piece);
                                mangled = substitute_path_macro(piece, "$system", system_dir);
+                       }
                        else
+                       {
+                               location->macro = NULL;
                                mangled = psprintf("%s/extension", piece);
-
+                       }
                        pfree(piece);
 
                        /* Canonicalize the path based on the OS and add to the list */
                        canonicalize_path(mangled);
-                       paths = lappend(paths, mangled);
+                       location->loc = mangled;
+                       paths = lappend(paths, location);
 
                        /* Break if ecp is empty or move to the next path on ecp */
                        if (ecp[len] == '\0')
@@ -2215,9 +2265,9 @@ pg_available_extensions(PG_FUNCTION_ARGS)
 
        locations = get_extension_control_directories();
 
-       foreach_ptr(char, location, locations)
+       foreach_ptr(ExtensionLocation, location, locations)
        {
-               dir = AllocateDir(location);
+               dir = AllocateDir(location->loc);
 
                /*
                 * If the control directory doesn't exist, we want to silently return
@@ -2229,13 +2279,13 @@ pg_available_extensions(PG_FUNCTION_ARGS)
                }
                else
                {
-                       while ((de = ReadDir(dir, location)) != NULL)
+                       while ((de = ReadDir(dir, location->loc)) != NULL)
                        {
                                ExtensionControlFile *control;
                                char       *extname;
                                String     *extname_str;
-                               Datum           values[3];
-                               bool            nulls[3];
+                               Datum           values[4];
+                               bool            nulls[4];
 
                                if (!is_extension_control_filename(de->d_name))
                                        continue;
@@ -2259,7 +2309,7 @@ pg_available_extensions(PG_FUNCTION_ARGS)
                                        found_ext = lappend(found_ext, extname_str);
 
                                control = new_ExtensionControlFile(extname);
-                               control->control_dir = pstrdup(location);
+                               control->control_dir = pstrdup(location->loc);
                                parse_extension_control_file(control, NULL);
 
                                memset(values, 0, sizeof(values));
@@ -2273,11 +2323,15 @@ pg_available_extensions(PG_FUNCTION_ARGS)
                                        nulls[1] = true;
                                else
                                        values[1] = CStringGetTextDatum(control->default_version);
+
+                               /* location */
+                               values[2] = CStringGetTextDatum(get_extension_location(location));
+
                                /* comment */
                                if (control->comment == NULL)
-                                       nulls[2] = true;
+                                       nulls[3] = true;
                                else
-                                       values[2] = CStringGetTextDatum(control->comment);
+                                       values[3] = CStringGetTextDatum(control->comment);
 
                                tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc,
                                                                         values, nulls);
@@ -2313,9 +2367,9 @@ pg_available_extension_versions(PG_FUNCTION_ARGS)
 
        locations = get_extension_control_directories();
 
-       foreach_ptr(char, location, locations)
+       foreach_ptr(ExtensionLocation, location, locations)
        {
-               dir = AllocateDir(location);
+               dir = AllocateDir(location->loc);
 
                /*
                 * If the control directory doesn't exist, we want to silently return
@@ -2327,7 +2381,7 @@ pg_available_extension_versions(PG_FUNCTION_ARGS)
                }
                else
                {
-                       while ((de = ReadDir(dir, location)) != NULL)
+                       while ((de = ReadDir(dir, location->loc)) != NULL)
                        {
                                ExtensionControlFile *control;
                                char       *extname;
@@ -2356,12 +2410,13 @@ pg_available_extension_versions(PG_FUNCTION_ARGS)
 
                                /* read the control file */
                                control = new_ExtensionControlFile(extname);
-                               control->control_dir = pstrdup(location);
+                               control->control_dir = pstrdup(location->loc);
                                parse_extension_control_file(control, NULL);
 
                                /* scan extension's script directory for install scripts */
                                get_available_versions_for_extension(control, rsinfo->setResult,
-                                                                                                        rsinfo->setDesc);
+                                                                                                        rsinfo->setDesc,
+                                                                                                        location);
                        }
 
                        FreeDir(dir);
@@ -2378,7 +2433,8 @@ pg_available_extension_versions(PG_FUNCTION_ARGS)
 static void
 get_available_versions_for_extension(ExtensionControlFile *pcontrol,
                                                                         Tuplestorestate *tupstore,
-                                                                        TupleDesc tupdesc)
+                                                                        TupleDesc tupdesc,
+                                                                        ExtensionLocation *location)
 {
        List       *evi_list;
        ListCell   *lc;
@@ -2391,8 +2447,8 @@ get_available_versions_for_extension(ExtensionControlFile *pcontrol,
        {
                ExtensionVersionInfo *evi = (ExtensionVersionInfo *) lfirst(lc);
                ExtensionControlFile *control;
-               Datum           values[8];
-               bool            nulls[8];
+               Datum           values[9];
+               bool            nulls[9];
                ListCell   *lc2;
 
                if (!evi->installable)
@@ -2428,11 +2484,15 @@ get_available_versions_for_extension(ExtensionControlFile *pcontrol,
                        nulls[6] = true;
                else
                        values[6] = convert_requires_to_datum(control->requires);
+
+               /* location */
+               values[7] = CStringGetTextDatum(get_extension_location(location));
+
                /* comment */
                if (control->comment == NULL)
-                       nulls[7] = true;
+                       nulls[8] = true;
                else
-                       values[7] = CStringGetTextDatum(control->comment);
+                       values[8] = CStringGetTextDatum(control->comment);
 
                tuplestore_putvalues(tupstore, tupdesc, values, nulls);
 
@@ -2473,7 +2533,7 @@ get_available_versions_for_extension(ExtensionControlFile *pcontrol,
                                        values[6] = convert_requires_to_datum(control->requires);
                                        nulls[6] = false;
                                }
-                               /* comment stays the same */
+                               /* comment and location stay the same */
 
                                tuplestore_putvalues(tupstore, tupdesc, values, nulls);
                        }
@@ -3903,7 +3963,8 @@ find_in_paths(const char *basename, List *paths)
 
        foreach(cell, paths)
        {
-               char       *path = lfirst(cell);
+               ExtensionLocation *location = lfirst(cell);
+               char       *path = location->loc;
                char       *full;
 
                Assert(path != NULL);
index 5f7886ab3ea7b5564012b91567647c714958234f..e25829fb05fa92f3610ac89db2b9e64925d0f7ff 100644 (file)
@@ -57,6 +57,6 @@
  */
 
 /*                                                     yyyymmddN */
-#define CATALOG_VERSION_NO     202512301
+#define CATALOG_VERSION_NO     202601011
 
 #endif
index 60f7ce502f6bc36d8f64be45f1695ae79251dac1..722fffc67f0e055138fa6e668d18b8f60c8dc00c 100644 (file)
 { oid => '3082', descr => 'list available extensions',
   proname => 'pg_available_extensions', procost => '10', prorows => '100',
   proretset => 't', provolatile => 's', prorettype => 'record',
-  proargtypes => '', proallargtypes => '{name,text,text}',
-  proargmodes => '{o,o,o}', proargnames => '{name,default_version,comment}',
+  proargtypes => '', proallargtypes => '{name,text,text,text}',
+  proargmodes => '{o,o,o,o}', proargnames => '{name,default_version,location,comment}',
   prosrc => 'pg_available_extensions' },
 { oid => '3083', descr => 'list available extension versions',
   proname => 'pg_available_extension_versions', procost => '10',
   prorows => '100', proretset => 't', provolatile => 's',
   prorettype => 'record', proargtypes => '',
-  proallargtypes => '{name,text,bool,bool,bool,name,_name,text}',
-  proargmodes => '{o,o,o,o,o,o,o,o}',
-  proargnames => '{name,version,superuser,trusted,relocatable,schema,requires,comment}',
+  proallargtypes => '{name,text,bool,bool,bool,name,_name,text,text}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o}',
+  proargnames => '{name,version,superuser,trusted,relocatable,schema,requires,location,comment}',
   prosrc => 'pg_available_extension_versions' },
 { oid => '3084', descr => 'list an extension\'s version update paths',
   proname => 'pg_extension_update_paths', procost => '10', prorows => '100',
index 7fbe5bde3322746d76cea6021e324b88430859da..bc9e4c99c8e6ab3a4bcd30e163138a9ff417f420 100644 (file)
@@ -25,6 +25,11 @@ my $ext_name2 = "test_custom_ext_paths_using_directory";
 mkpath("$ext_dir/$ext_name2");
 create_extension($ext_name2, $ext_dir, $ext_name2);
 
+# Make windows path use Unix slashes as canonicalize_path() is called when
+# collecting extension control paths. See get_extension_control_directories().
+my $ext_dir_canonicalized = $ext_dir;
+$ext_dir_canonicalized =~ s!\\!/!g if $windows_os;
+
 # Use the correct separator and escape \ when running on Windows.
 my $sep = $windows_os ? ";" : ":";
 $node->append_conf(
@@ -35,6 +40,10 @@ extension_control_path = '\$system$sep@{[ $windows_os ? ($ext_dir =~ s/\\/\\\\/g
 # Start node
 $node->start;
 
+# Create an user to test permissions to read extension locations.
+my $user = "user01";
+$node->safe_psql('postgres', "CREATE USER $user");
+
 my $ecp = $node->safe_psql('postgres', 'show extension_control_path;');
 
 is($ecp, "\$system$sep$ext_dir$sep$ext_dir2",
@@ -46,48 +55,65 @@ $node->safe_psql('postgres', "CREATE EXTENSION $ext_name2");
 my $ret = $node->safe_psql('postgres',
        "select * from pg_available_extensions where name = '$ext_name'");
 is( $ret,
-       "test_custom_ext_paths|1.0|1.0|Test extension_control_path",
-       "extension is installed correctly on pg_available_extensions");
+       "test_custom_ext_paths|1.0|1.0|$ext_dir_canonicalized/extension|Test extension_control_path",
+       "extension is shown correctly in pg_available_extensions");
 
 $ret = $node->safe_psql('postgres',
        "select * from pg_available_extension_versions where name = '$ext_name'");
 is( $ret,
-       "test_custom_ext_paths|1.0|t|t|f|t|||Test extension_control_path",
-       "extension is installed correctly on pg_available_extension_versions");
+       "test_custom_ext_paths|1.0|t|t|f|t|||$ext_dir_canonicalized/extension|Test extension_control_path",
+       "extension is shown correctly in pg_available_extension_versions");
 
 $ret = $node->safe_psql('postgres',
        "select * from pg_available_extensions where name = '$ext_name2'");
 is( $ret,
-       "test_custom_ext_paths_using_directory|1.0|1.0|Test extension_control_path",
-       "extension is installed correctly on pg_available_extensions");
+       "test_custom_ext_paths_using_directory|1.0|1.0|$ext_dir_canonicalized/extension|Test extension_control_path",
+       "extension is shown correctly in pg_available_extensions");
 
 $ret = $node->safe_psql('postgres',
        "select * from pg_available_extension_versions where name = '$ext_name2'"
 );
 is( $ret,
-       "test_custom_ext_paths_using_directory|1.0|t|t|f|t|||Test extension_control_path",
-       "extension is installed correctly on pg_available_extension_versions");
+       "test_custom_ext_paths_using_directory|1.0|t|t|f|t|||$ext_dir_canonicalized/extension|Test extension_control_path",
+       "extension is shown correctly in pg_available_extension_versions");
+
+# Test that a non-superuser is not able to read the extension location in
+# pg_available_extensions
+$ret = $node->safe_psql('postgres',
+       "select location from pg_available_extensions where name = '$ext_name2'",
+       connstr => "user=$user");
+is( $ret,
+       "<insufficient privilege>",
+       "extension location is hidden in pg_available_extensions for users with insufficient privilege");
 
-# Ensure that extensions installed on $system is still visible when using with
+# Test that a non-superuser is not able to read the extension location in
+# pg_available_extension_versions
+$ret = $node->safe_psql('postgres',
+       "select location from pg_available_extension_versions where name = '$ext_name2'",
+       connstr => "user=$user");
+is( $ret,
+       "<insufficient privilege>",
+       "extension location is hidden in pg_available_extension_versions for users with insufficient privilege");
+
+# Ensure that extensions installed in $system are still visible when used with
 # custom extension control path.
 $ret = $node->safe_psql('postgres',
        "select count(*) > 0 as ok from pg_available_extensions where name = 'plpgsql'"
 );
 is($ret, "t",
-       "\$system extension is installed correctly on pg_available_extensions");
-
+       "\$system extension is shown correctly in pg_available_extensions");
 
 $ret = $node->safe_psql('postgres',
        "set extension_control_path = ''; select count(*) > 0 as ok from pg_available_extensions where name = 'plpgsql'"
 );
 is($ret, "t",
-       "\$system extension is installed correctly on pg_available_extensions with empty extension_control_path"
+       "\$system extension is shown correctly in pg_available_extensions with empty extension_control_path"
 );
 
 # Test with an extension that does not exists
 my ($code, $stdout, $stderr) =
   $node->psql('postgres', "CREATE EXTENSION invalid");
-is($code, 3, 'error to create an extension that does not exists');
+is($code, 3, 'error creating an extension that does not exist');
 like($stderr, qr/ERROR:  extension "invalid" is not available/);
 
 sub create_extension
index 4286c266e17f39dca9e66a356165793061b0a30f..f4ee2bd7459e63c3df46f9aad50d6b0cadaa7886 100644 (file)
@@ -1310,14 +1310,16 @@ pg_available_extension_versions| SELECT e.name,
     e.relocatable,
     e.schema,
     e.requires,
+    e.location,
     e.comment
-   FROM (pg_available_extension_versions() e(name, version, superuser, trusted, relocatable, schema, requires, comment)
+   FROM (pg_available_extension_versions() e(name, version, superuser, trusted, relocatable, schema, requires, location, comment)
      LEFT JOIN pg_extension x ON (((e.name = x.extname) AND (e.version = x.extversion))));
 pg_available_extensions| SELECT e.name,
     e.default_version,
     x.extversion AS installed_version,
+    e.location,
     e.comment
-   FROM (pg_available_extensions() e(name, default_version, comment)
+   FROM (pg_available_extensions() e(name, default_version, location, comment)
      LEFT JOIN pg_extension x ON ((e.name = x.extname)));
 pg_backend_memory_contexts| SELECT name,
     ident,
index ceb3fc5d980424fec1c30b15f99fb3ce5aa1c355..b9e671fcda85a9c813add5d90ea1fd14a9590882 100644 (file)
@@ -800,6 +800,7 @@ ExtensibleNodeEntry
 ExtensibleNodeMethods
 ExtensionControlFile
 ExtensionInfo
+ExtensionLocation
 ExtensionVersionInfo
 FDWCollateState
 FD_SET
@@ -1583,7 +1584,6 @@ LoadStmt
 LocalBufferLookupEnt
 LocalPgBackendStatus
 LocalTransactionId
-Location
 LocationIndex
 LocationLen
 LockAcquireResult