From 0a19076958b684fd98d65fb97f4b228d2ddd8b2c Mon Sep 17 00:00:00 2001
From: Matheus Alcantara <mths.dev@pm.me>
Date: Wed, 23 Apr 2025 16:11:24 -0300
Subject: [PATCH v3] Make "directory" work with extension control path

Previously extensions installed on a custom path that is available via
extension_control_path GUC that set the "directory" field on .control
file was not being able to CREATE. This was happening because on
get_extension_script_directory was hard coded to search for the script
files only on the share system dir.

This commit fix this issue by using the control->control_dir as a share
dir to return the path of the extension script files.
---
 src/backend/commands/extension.c              | 31 +++++--
 .../t/001_extension_control_path.pl           | 93 ++++++++++++++-----
 2 files changed, 92 insertions(+), 32 deletions(-)

diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c
index 180f4af9be3..ce53ed85401 100644
--- a/src/backend/commands/extension.c
+++ b/src/backend/commands/extension.c
@@ -83,6 +83,8 @@ Oid			CurrentExtensionObject = InvalidOid;
 typedef struct ExtensionControlFile
 {
 	char	   *name;			/* name of the extension */
+	char	   *basedir;		/* base directory where control and script
+								 * files are located */
 	char	   *control_dir;	/* directory where control file was found */
 	char	   *directory;		/* directory for script files */
 	char	   *default_version;	/* default install target version, if any */
@@ -376,6 +378,14 @@ get_extension_control_directories(void)
 
 			/* Substitute the path macro if needed */
 			mangled = substitute_path_macro(piece, "$system", system_dir);
+
+			/*
+			 * Append "extension" suffix in case is a custom extension control
+			 * path.
+			 */
+			if (strcmp(piece, "$system") != 0)
+				mangled = psprintf("%s/extension", mangled);
+
 			pfree(piece);
 
 			/* Canonicalize the path based on the OS and add to the list */
@@ -422,6 +432,9 @@ find_extension_control_filename(ExtensionControlFile *control)
 	ecp = Extension_control_path;
 	if (strlen(ecp) == 0)
 		ecp = "$system";
+	else if (strcmp(ecp, "$system") != 0)
+		ecp = psprintf("%s/extension", ecp);
+
 	result = find_in_path(basename, ecp, "extension_control_path", "$system", system_dir);
 
 	if (result)
@@ -439,9 +452,6 @@ find_extension_control_filename(ExtensionControlFile *control)
 static char *
 get_extension_script_directory(ExtensionControlFile *control)
 {
-	char		sharepath[MAXPGPATH];
-	char	   *result;
-
 	/*
 	 * The directory parameter can be omitted, absolute, or relative to the
 	 * installation's share directory.
@@ -452,11 +462,8 @@ get_extension_script_directory(ExtensionControlFile *control)
 	if (is_absolute_path(control->directory))
 		return pstrdup(control->directory);
 
-	get_share_path(my_exec_path, sharepath);
-	result = (char *) palloc(MAXPGPATH);
-	snprintf(result, MAXPGPATH, "%s/%s", sharepath, control->directory);
-
-	return result;
+	Assert(control->basedir != NULL);
+	return psprintf("%s/%s", control->basedir, control->directory);
 }
 
 static char *
@@ -550,6 +557,14 @@ parse_extension_control_file(ExtensionControlFile *control,
 				 errhint("The extension must first be installed on the system where PostgreSQL is running.")));
 	}
 
+	/* Assert that the control_dir ends with /extension */
+	Assert(control->control_dir != NULL);
+	Assert(strcmp(control->control_dir + strlen(control->control_dir) - strlen("/extension"), "/extension") == 0);
+
+	control->basedir = pnstrdup(
+								control->control_dir,
+								strlen(control->control_dir) - strlen("/extension"));
+
 	if ((file = AllocateFile(filename, "r")) == NULL)
 	{
 		/* no complaint for missing auxiliary file */
diff --git a/src/test/modules/test_extensions/t/001_extension_control_path.pl b/src/test/modules/test_extensions/t/001_extension_control_path.pl
index c186c1470f7..1ef79d7574f 100644
--- a/src/test/modules/test_extensions/t/001_extension_control_path.pl
+++ b/src/test/modules/test_extensions/t/001_extension_control_path.pl
@@ -5,6 +5,7 @@ use warnings FATAL => 'all';
 use PostgreSQL::Test::Cluster;
 use PostgreSQL::Test::Utils;
 use Test::More;
+use File::Path qw( make_path );
 
 my $node = PostgreSQL::Test::Cluster->new('node');
 
@@ -12,25 +13,14 @@ $node->init;
 
 # Create a temporary directory for the extension control file
 my $ext_dir = PostgreSQL::Test::Utils::tempdir();
+make_path("$ext_dir/extension");
+
 my $ext_name = "test_custom_ext_paths";
-my $control_file = "$ext_dir/$ext_name.control";
-my $sql_file = "$ext_dir/$ext_name--1.0.sql";
-
-# Create .control .sql file
-open my $cf, '>', $control_file or die "Could not create control file: $!";
-print $cf "comment = 'Test extension_control_path'\n";
-print $cf "default_version = '1.0'\n";
-print $cf "relocatable = true\n";
-close $cf;
-
-# Create --1.0.sql file
-open my $sqlf, '>', $sql_file or die "Could not create sql file: $!";
-print $sqlf "/* $sql_file */\n";
-print $sqlf
-  "-- complain if script is sourced in psql, rather than via CREATE EXTENSION\n";
-print $sqlf
-  qq'\\echo Use "CREATE EXTENSION $ext_name" to load this file. \\quit\n';
-close $sqlf;
+create_extension($ext_name, $ext_dir);
+
+my $ext_name2 = "test_custom_ext_paths_using_directory";
+make_path("$ext_dir/$ext_name2");
+create_extension($ext_name2, $ext_dir, $ext_name2);
 
 # Use the correct separator and escape \ when running on Windows.
 my $sep = $windows_os ? ";" : ":";
@@ -48,6 +38,7 @@ is($ecp, "\$system$sep$ext_dir",
 	"custom extension control directory path configured");
 
 $node->safe_psql('postgres', "CREATE EXTENSION $ext_name");
+$node->safe_psql('postgres', "CREATE EXTENSION $ext_name2");
 
 my $ret = $node->safe_psql('postgres',
 	"select * from pg_available_extensions where name = '$ext_name'");
@@ -55,26 +46,80 @@ is( $ret,
 	"test_custom_ext_paths|1.0|1.0|Test extension_control_path",
 	"extension is installed correctly on pg_available_extensions");
 
-my $ret2 = $node->safe_psql('postgres',
+$ret = $node->safe_psql('postgres',
 	"select * from pg_available_extension_versions where name = '$ext_name'");
-is( $ret2,
+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");
 
+$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");
+
+$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");
+
 # Ensure that extensions installed on $system is still visible when using with
 # custom extension control path.
-my $ret3 = $node->safe_psql('postgres',
+$ret = $node->safe_psql('postgres',
 	"select count(*) > 0 as ok from pg_available_extensions where name = 'plpgsql'"
 );
-is($ret3, "t",
+is($ret, "t",
 	"\$system extension is installed correctly on pg_available_extensions");
 
 
-my $ret4 = $node->safe_psql('postgres',
+$ret = $node->safe_psql('postgres',
 	"set extension_control_path = ''; select count(*) > 0 as ok from pg_available_extensions where name = 'plpgsql'"
 );
-is($ret4, "t",
+is($ret, "t",
 	"\$system extension is installed correctly on 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');
+like($stderr, qr/ERROR:  extension "invalid" is not available/);
+
+sub create_extension
+{
+	my ($ext_name, $ext_dir, $directory) = @_;
+
+	my $control_file = "$ext_dir/extension/$ext_name.control";
+	my $sql_file;
+
+	if (defined $directory)
+	{
+		$sql_file = "$ext_dir/$directory/$ext_name--1.0.sql";
+	}
+	else
+	{
+		$sql_file = "$ext_dir/extension/$ext_name--1.0.sql";
+	}
+
+	# Create .control .sql file
+	open my $cf, '>', $control_file or die "Could not create control file: $!";
+	print $cf "comment = 'Test extension_control_path'\n";
+	print $cf "default_version = '1.0'\n";
+	print $cf "relocatable = true\n";
+	if (defined $directory)
+	{
+		print $cf "directory = $directory";
+	}
+	close $cf;
+
+	# Create --1.0.sql file
+	open my $sqlf, '>', $sql_file or die "Could not create sql file: $!";
+	print $sqlf "/* $sql_file */\n";
+	print $sqlf
+	  "-- complain if script is sourced in psql, rather than via CREATE EXTENSION\n";
+	print $sqlf
+	  qq'\\echo Use "CREATE EXTENSION $ext_name" to load this file. \\quit\n';
+	close $sqlf;
+}
+
 done_testing();
-- 
2.39.5 (Apple Git-154)

