Always better with a file attached :-/

Sorry for the noise.

--
Fabien.
________________________________
De : Fabien COELHO <coe...@cri.ensmp.fr>
Envoyé : lundi 31 mars 2025 13:48
À : PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>
Objet : Add partial :-variable expansion to psql \copy

Hello,

I've been biten by psql's \copy lack of variable expansion, in a limited-access 
docker-inside-VM context where COPY is not a viable option and hardwired names 
are not desirable. The attached patch allows \copy to use variable's values in 
place of table and file names:

```psql
\set table 'some table'
\set input 'some file name.csv'
\copy :"table" from :'input' with (format csv)
```

--
Fabien.
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index f7c8bc16a7f..445443613bc 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1165,8 +1165,8 @@ SELECT $1 \parse stmt1
         Because of this, special parsing rules apply to the <command>\copy</command>
         meta-command.  Unlike most other meta-commands, the entire remainder
         of the line is always taken to be the arguments of <command>\copy</command>,
-        and neither variable interpolation nor backquote expansion are
-        performed in the arguments.
+        variable interpolation is restricted to the table and file names only,
+        and backquote expansion is not performed in the arguments.
         </para>
 
         <tip>
@@ -1178,7 +1178,9 @@ SELECT $1 \parse stmt1
         or <literal>\g |<replaceable>program</replaceable></literal>.
         Unlike <literal>\copy</literal>, this method allows the command to
         span multiple lines; also, variable interpolation and backquote
-        expansion can be used.
+        expansion can be used; however, <literal>COPY</literal> interacts
+        with the server-side file system, so it does not work with a remote
+        connection and client-side files.
         </para>
         </tip>
 
diff --git a/src/bin/psql/copy.c b/src/bin/psql/copy.c
index 92c955b637a..aa030c33d5f 100644
--- a/src/bin/psql/copy.c
+++ b/src/bin/psql/copy.c
@@ -104,7 +104,7 @@ parse_slash_copy(const char *args)
 	result->before_tofrom = pg_strdup("");	/* initialize for appending */
 
 	token = strtokx(args, whitespace, ".,()", "\"",
-					0, false, false, pset.encoding);
+					0, false, false, true, pset.encoding);
 	if (!token)
 		goto error;
 
@@ -113,7 +113,7 @@ parse_slash_copy(const char *args)
 	{
 		xstrcat(&result->before_tofrom, token);
 		token = strtokx(NULL, whitespace, ".,()", "\"",
-						0, false, false, pset.encoding);
+						0, false, false, true, pset.encoding);
 		if (!token)
 			goto error;
 	}
@@ -128,7 +128,8 @@ parse_slash_copy(const char *args)
 			xstrcat(&result->before_tofrom, " ");
 			xstrcat(&result->before_tofrom, token);
 			token = strtokx(NULL, whitespace, "()", "\"'",
-							nonstd_backslash, true, false, pset.encoding);
+							nonstd_backslash, true, false, false,
+							pset.encoding);
 			if (!token)
 				goto error;
 			if (token[0] == '(')
@@ -141,7 +142,7 @@ parse_slash_copy(const char *args)
 	xstrcat(&result->before_tofrom, " ");
 	xstrcat(&result->before_tofrom, token);
 	token = strtokx(NULL, whitespace, ".,()", "\"",
-					0, false, false, pset.encoding);
+					0, false, false, false, pset.encoding);
 	if (!token)
 		goto error;
 
@@ -154,12 +155,12 @@ parse_slash_copy(const char *args)
 		/* handle schema . table */
 		xstrcat(&result->before_tofrom, token);
 		token = strtokx(NULL, whitespace, ".,()", "\"",
-						0, false, false, pset.encoding);
+						0, false, false, false, pset.encoding);
 		if (!token)
 			goto error;
 		xstrcat(&result->before_tofrom, token);
 		token = strtokx(NULL, whitespace, ".,()", "\"",
-						0, false, false, pset.encoding);
+						0, false, false, false, pset.encoding);
 		if (!token)
 			goto error;
 	}
@@ -172,7 +173,7 @@ parse_slash_copy(const char *args)
 			xstrcat(&result->before_tofrom, " ");
 			xstrcat(&result->before_tofrom, token);
 			token = strtokx(NULL, whitespace, "()", "\"",
-							0, false, false, pset.encoding);
+							0, false, false, false, pset.encoding);
 			if (!token)
 				goto error;
 			if (token[0] == ')')
@@ -181,7 +182,7 @@ parse_slash_copy(const char *args)
 		xstrcat(&result->before_tofrom, " ");
 		xstrcat(&result->before_tofrom, token);
 		token = strtokx(NULL, whitespace, ".,()", "\"",
-						0, false, false, pset.encoding);
+						0, false, false, false, pset.encoding);
 		if (!token)
 			goto error;
 	}
@@ -193,9 +194,9 @@ parse_slash_copy(const char *args)
 	else
 		goto error;
 
-	/* { 'filename' | PROGRAM 'command' | STDIN | STDOUT | PSTDIN | PSTDOUT } */
+	/* { 'filename' | :varname | PROGRAM 'command' | STDIN | STDOUT | PSTDIN | PSTDOUT } */
 	token = strtokx(NULL, whitespace, ";", "'",
-					0, false, false, pset.encoding);
+					0, false, false, true, pset.encoding);
 	if (!token)
 		goto error;
 
@@ -204,7 +205,7 @@ parse_slash_copy(const char *args)
 		int			toklen;
 
 		token = strtokx(NULL, whitespace, ";", "'",
-						0, false, false, pset.encoding);
+						0, false, false, true, pset.encoding);
 		if (!token)
 			goto error;
 
@@ -242,7 +243,7 @@ parse_slash_copy(const char *args)
 
 	/* Collect the rest of the line (COPY options) */
 	token = strtokx(NULL, "", NULL, NULL,
-					0, false, false, pset.encoding);
+					0, false, false, false, pset.encoding);
 	if (token)
 		result->after_tofrom = pg_strdup(token);
 
diff --git a/src/bin/psql/stringutils.c b/src/bin/psql/stringutils.c
index f613413553e..41ecfc669fd 100644
--- a/src/bin/psql/stringutils.c
+++ b/src/bin/psql/stringutils.c
@@ -30,6 +30,7 @@
  * e_strings -	if true, treat E'...' syntax as a valid token
  * del_quotes - if true, strip quotes from the returned token, else return
  *				it exactly as found in the string
+ * variable -   if true, allow token from :-variable
  * encoding -	the active character-set encoding
  *
  * Characters in 'delim', if any, will be returned as single-character
@@ -56,8 +57,10 @@ strtokx(const char *s,
 		char escape,
 		bool e_strings,
 		bool del_quotes,
+		bool variable,
 		int encoding)
 {
+	static size_t storage_size = 0;    /* allocated size of storage */
 	static char *storage = NULL;	/* store the local copy of the users
 									 * string here */
 	static char *string = NULL; /* pointer into storage where to continue on
@@ -77,7 +80,8 @@ strtokx(const char *s,
 		 * tokens.  2X the space is a gross overestimate, but it's unlikely
 		 * that this code will be used on huge strings anyway.
 		 */
-		storage = pg_malloc(2 * strlen(s) + 1);
+		storage_size = 2 * strlen(s) + 1;
+		storage = pg_malloc(storage_size);
 		strcpy(storage, s);
 		string = storage;
 	}
@@ -96,9 +100,86 @@ strtokx(const char *s,
 		free(storage);
 		storage = NULL;
 		string = NULL;
+		storage_size = 0;
 		return NULL;
 	}
 
+	/* handle a :-variable name */
+	if (variable && *start == ':')
+	{
+		char *varname = start + 1;
+		char *value;
+		char *next;
+		char qc = '\0';
+
+		/* :'var' or :"var" */
+		if (quote && strchr(quote, *varname))
+		{
+			qc = *varname;
+			p = varname = varname + 1;
+
+			while (*p && *p != qc)
+				p++;
+
+			/* no end quote, parse error */
+			if (! *p)
+				return NULL;
+
+			/* end quote is end of variable name */
+			*p = '\0';
+
+			next = p+1;
+		}
+		else  /* :var */
+		{
+			p = varname;
+
+			/* find end-of variable */
+			while (*p && strchr(whitespace, *p))
+				p++;
+
+			if (*p)  /* space */
+			{
+				*p = '\0';
+				next = p+1;
+			}
+			else  /* storage end there */
+				next = p;
+		}
+
+		value = psql_get_variable(varname,
+								  qc == '\'' ? PQUOTE_SQL_LITERAL :
+								  qc == '"' ? PQUOTE_SQL_IDENT :
+								  PQUOTE_PLAIN,
+								  NULL);
+
+		if (!value)  /* unexpected undefined variable, parse error */
+			return NULL;
+
+		/* deal with malloc'ed value by embedding it in storage */
+		if (strlen(string) < strlen(value))
+		{
+			int shift = strlen(value) - strlen(start);
+			int string_offset = string - storage;
+			int next_offset = next - storage;
+			int remainder = storage_size - next_offset;
+
+			storage_size += shift;
+			storage = pg_realloc(storage, storage_size);
+			string = storage + string_offset;
+			next = storage + next_offset;
+			memmove(next + shift, next, remainder);
+			next += shift;
+		}
+
+		strcpy(string, value);
+		free(value);
+		start = string;
+		string = next;
+
+		return start;
+	}
+
 	/* test if delimiter character */
 	if (delim && strchr(delim, *start))
 	{
diff --git a/src/bin/psql/stringutils.h b/src/bin/psql/stringutils.h
index 5b91e964103..560139dc2f0 100644
--- a/src/bin/psql/stringutils.h
+++ b/src/bin/psql/stringutils.h
@@ -17,6 +17,7 @@ extern char *strtokx(const char *s,
 					 char escape,
 					 bool e_strings,
 					 bool del_quotes,
+					 bool variable,
 					 int encoding);
 
 extern void strip_quotes(char *source, char quote, char escape, int encoding);
diff --git a/src/bin/psql/t/001_basic.pl b/src/bin/psql/t/001_basic.pl
index 739cb439708..70eb35d6bbc 100644
--- a/src/bin/psql/t/001_basic.pl
+++ b/src/bin/psql/t/001_basic.pl
@@ -364,6 +364,21 @@ psql_like(
 3|test|2022-07-05 00:00:00/,
 	'\copy from with DEFAULT');
 
+# Test copy with :-variables, reusing previous table
+my $more_csv_data = "$tempdir/copy more data.csv";
+append_to_file($more_csv_data, "4,Susie,1970-10-14\n");
+append_to_file($more_csv_data, "5,Hobbes,2020-07-29\n");
+
+psql_like(
+	$node,
+	"TRUNCATE copy_default;
+	\\set table copy_default
+	\\set input '$more_csv_data'
+	\\copy :\"table\" from :'input' with (format 'csv')
+	SELECT * FROM :table WHERE id = 5;",
+	qr/5|Hobbes|2020-07-29/,
+	'\copy using :-variables');
+
 # Check \watch
 # Note: the interval value is parsed with locale-aware strtod()
 psql_like(

Reply via email to