From 52a6c9ac0777be6c67307d1b5cdee93acb475df8 Mon Sep 17 00:00:00 2001
From: Joel Jacobson <joel@compiler.org>
Date: Thu, 24 Oct 2024 09:16:31 +0300
Subject: [PATCH 2/3] Add raw format to COPY command.

This commit introduces a new raw format to the COPY command, enabling
efficient bulk data transfer of a single text column without any parsing,
quoting, or escaping. In raw format, data is copied exactly as it appears
in the file or table, adhering to the specified ENCODING option or the
current client encoding.

The raw format enforces a single column requirement, ensuring that exactly
one column is specified in the column list. Attempts to specify multiple
columns or omit the column list when the table has multiple columns will
result in an error. Additionally, the DELIMITER option in raw format accepts
any string, including multi-byte characters, providing greater flexibility
in defining data separators. If no DELIMITER is specified, the entire input
or output is treated as a single data value.

Furthermore, the raw format does not support format-specific options such as
NULL, HEADER, QUOTE, ESCAPE, FORCE_QUOTE, FORCE_NOT_NULL, and FORCE_NULL.
Using these options with the raw format will trigger errors, ensuring that
data remains unaltered during the transfer process.

This enhancement is particularly useful when handling text blobs, JSON files,
or other text-based formats where preserving the data "as is" is crucial.
---
 doc/src/sgml/ref/copy.sgml               | 134 ++++++-
 src/backend/commands/copy.c              |  89 +++--
 src/backend/commands/copyfrom.c          |   7 +
 src/backend/commands/copyfromparse.c     | 431 +++++++++++++----------
 src/backend/commands/copyto.c            |  90 ++++-
 src/bin/psql/tab-complete.in.c           |   2 +-
 src/include/commands/copy.h              |   3 +-
 src/include/commands/copyfrom_internal.h |   1 +
 src/test/regress/expected/copy.out       |  52 +++
 src/test/regress/expected/copy2.out      |  52 ++-
 src/test/regress/sql/copy.sql            |  24 ++
 src/test/regress/sql/copy2.sql           |  37 +-
 12 files changed, 691 insertions(+), 231 deletions(-)

diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml
index 8394402f09..f17d606537 100644
--- a/doc/src/sgml/ref/copy.sgml
+++ b/doc/src/sgml/ref/copy.sgml
@@ -218,8 +218,9 @@ COPY { <replaceable class="parameter">table_name</replaceable> [ ( <replaceable
      <para>
       Selects the data format to be read or written:
       <literal>text</literal>,
-      <literal>csv</literal> (Comma Separated Values),
-      or <literal>binary</literal>.
+      <literal>CSV</literal> (Comma Separated Values),
+      <literal>binary</literal>,
+      or <literal>raw</literal>
       The default is <literal>text</literal>.
       See <xref linkend="sql-copy-file-formats"/> below for details.
      </para>
@@ -253,11 +254,27 @@ COPY { <replaceable class="parameter">table_name</replaceable> [ ( <replaceable
     <term><literal>DELIMITER</literal></term>
     <listitem>
      <para>
-      Specifies the character that separates columns within each row
-      (line) of the file.  The default is a tab character in text format,
-      a comma in <literal>CSV</literal> format.
-      This must be a single one-byte character.
-      This option is not allowed when using <literal>binary</literal> format.
+      Specifies the delimiter used in the file. Its usage depends on the
+      <literal>FORMAT</literal> specified:
+      <simplelist>
+       <member>
+        In <literal>text</literal> and <literal>CSV</literal> formats,
+        the delimiter separates <emphasis>columns</emphasis> within each row
+        (line) of the file.
+        The default is a tab character in <literal>text</literal> format and
+        a comma in <literal>CSV</literal> format. This must be a single
+        one-byte character.
+       </member>
+       <member>
+        In <literal>raw</literal> format, the delimiter separates
+        <emphasis>rows</emphasis> in the file. The default is no delimiter,
+        which means that for <command>COPY FROM</command>, the entire input is
+        read as a single field, and for <command>COPY TO</command>, the output
+        is concatenated without any delimiter. If a delimiter is specified,
+        it can be a multi-byte string; for example, <literal>E'\r\n'</literal>
+        can be used when dealing with text files on Windows platforms.
+       </member>
+      </simplelist>
      </para>
     </listitem>
    </varlistentry>
@@ -271,7 +288,8 @@ COPY { <replaceable class="parameter">table_name</replaceable> [ ( <replaceable
       string in <literal>CSV</literal> format. You might prefer an
       empty string even in text format for cases where you don't want to
       distinguish nulls from empty strings.
-      This option is not allowed when using <literal>binary</literal> format.
+      This option is allowed only when using <literal>text</literal> or
+      <literal>CSV</literal> format.
      </para>
 
      <note>
@@ -294,7 +312,7 @@ COPY { <replaceable class="parameter">table_name</replaceable> [ ( <replaceable
       is found in the input file, the default value of the corresponding column
       will be used.
       This option is allowed only in <command>COPY FROM</command>, and only when
-      not using <literal>binary</literal> format.
+      using <literal>text</literal> or <literal>CSV</literal> format.
      </para>
     </listitem>
    </varlistentry>
@@ -310,7 +328,8 @@ COPY { <replaceable class="parameter">table_name</replaceable> [ ( <replaceable
       If this option is set to <literal>MATCH</literal>, the number and names
       of the columns in the header line must match the actual column names of
       the table, in order;  otherwise an error is raised.
-      This option is not allowed when using <literal>binary</literal> format.
+      This option is allowed only when using <literal>text</literal> or
+      <literal>CSV</literal> format.
       The <literal>MATCH</literal> option is only valid for <command>COPY
       FROM</command> commands.
      </para>
@@ -400,7 +419,8 @@ COPY { <replaceable class="parameter">table_name</replaceable> [ ( <replaceable
      </para>
      <para>
       The <literal>ignore</literal> option is applicable only for <command>COPY FROM</command>
-      when the <literal>FORMAT</literal> is <literal>text</literal> or <literal>csv</literal>.
+      when the <literal>FORMAT</literal> is <literal>text</literal>,
+      <literal>CSV</literal> or <literal>raw</literal>.
      </para>
      <para>
       A <literal>NOTICE</literal> message containing the ignored row count is
@@ -893,6 +913,98 @@ COPY <replaceable class="parameter">count</replaceable>
 
   </refsect2>
 
+  <refsect2 id="sql-copy-raw-format" xreflabel="Raw Format">
+   <title>Raw Format</title>
+
+   <para>
+    The <literal>raw</literal> format is designed for efficient bulk data
+    transfer of a single text column without any parsing, quoting, or
+    escaping. In this format, data is copied exactly as it appears in the file
+    or table, interpreted according to the specified <literal>ENCODING</literal>
+    option or the current client encoding.
+   </para>
+
+   <para>
+    When using the <literal>raw</literal> format, each data value corresponds
+    to a single field with no additional formatting or processing. The
+    <literal>DELIMITER</literal> option specifies the string that separates
+    data values. Unlike in other formats, the delimiter in
+    <literal>raw</literal> format can be any string, including multi-byte
+    characters. If no <literal>DELIMITER</literal> is specified, the entire
+    input or output is treated as a single data value.
+   </para>
+
+   <para>
+    The <literal>raw</literal> format requires that exactly one column be
+    specified in the column list. An error is raised if more than one column
+    is specified or if no column list is specified when the table has multiple
+    columns.
+   </para>
+
+   <para>
+    The <literal>raw</literal> format does not support any of the
+    format-specific options of other formats, such as <literal>NULL</literal>,
+    <literal>HEADER</literal>, <literal>QUOTE</literal>,
+    <literal>ESCAPE</literal>, <literal>FORCE_QUOTE</literal>,
+    <literal>FORCE_NOT_NULL</literal>, and <literal>FORCE_NULL</literal>.
+    Attempting to use these options with <literal>raw</literal> format will
+    result in an error.
+   </para>
+
+   <para>
+    Since the <literal>raw</literal> format deals with text, the data is
+    interpreted according to the specified <literal>ENCODING</literal> option
+    or the current client encoding for input, and encoded using the specified
+    <literal>ENCODING</literal> or the current client encoding for output.
+   </para>
+
+   <note>
+    <para>
+     Empty lines in the input are treated as empty strings, not as
+     <literal>NULL</literal> values. There is no way to represent a
+     <literal>NULL</literal> value in <literal>raw</literal> format.
+    </para>
+   </note>
+
+   <note>
+    <para>
+     The <literal>raw</literal> format is particularly useful when you need to
+     import or export data exactly as it appears. This can be
+     helpful when dealing with large text blobs, JSON files, or other
+     text-based formats.
+    </para>
+   </note>
+
+   <note>
+    <para>
+     The <literal>raw</literal> format can only be used when copying exactly
+     one column. If the table has multiple columns, you must specify the
+     column list containing only one column.
+    </para>
+   </note>
+
+   <note>
+    <para>
+     Unlike other formats, the delimiter in <literal>raw</literal> format can
+     be any string, and there are no restrictions on the characters used in
+     the delimiter, including newline or carriage return characters.
+    </para>
+   </note>
+
+   <note>
+    <para>
+     When using <literal>COPY TO</literal> with <literal>raw</literal> format
+     and a specified <literal>DELIMITER</literal>, there is no check to prevent
+     data values from containing the delimiter string, which could be
+     problematic if it would be needed to import the data preserved using
+     <literal>COPY FROM</literal>, since a data value containing the delimiter
+     would then be split into two values. If this is a concern, a different
+     format should be used instead.
+    </para>
+   </note>
+  </refsect2>
+
+
   <refsect2 id="sql-copy-binary-format" xreflabel="Binary Format">
    <title>Binary Format</title>
 
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index b7e819de40..bb3b106ff1 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -516,6 +516,8 @@ ProcessCopyOptions(ParseState *pstate,
 				opts_out->format = COPY_FORMAT_CSV;
 			else if (strcmp(fmt, "binary") == 0)
 				opts_out->format = COPY_FORMAT_BINARY;
+			else if (strcmp(fmt, "raw") == 0)
+				opts_out->format = COPY_FORMAT_RAW;
 			else
 				ereport(ERROR,
 						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
@@ -686,18 +688,61 @@ ProcessCopyOptions(ParseState *pstate,
 				(errcode(ERRCODE_SYNTAX_ERROR),
 				 errmsg("cannot specify %s in BINARY mode", "NULL")));
 
+	if (opts_out->format == COPY_FORMAT_RAW && opts_out->null_print)
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("cannot specify %s in RAW mode", "NULL")));
+
 	if (opts_out->format == COPY_FORMAT_BINARY && opts_out->default_print)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
 				 errmsg("cannot specify %s in BINARY mode", "DEFAULT")));
 
+	if (opts_out->format == COPY_FORMAT_RAW && opts_out->default_print)
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("cannot specify %s in RAW mode", "DEFAULT")));
+
+	if (opts_out->delim)
+	{
+		if (opts_out->format != COPY_FORMAT_RAW)
+		{
+			/* Only single-byte delimiter strings are supported. */
+			if (strlen(opts_out->delim) != 1)
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("COPY delimiter must be a single one-byte character")));
+
+			/* Disallow end-of-line characters */
+			if (strchr(opts_out->delim, '\r') != NULL ||
+				strchr(opts_out->delim, '\n') != NULL)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("COPY delimiter cannot be newline or carriage return")));
+		}
+	}
 	/* Set defaults for omitted options */
-	if (!opts_out->delim)
-		opts_out->delim = opts_out->format == COPY_FORMAT_CSV ? "," : "\t";
+	else if (opts_out->format == COPY_FORMAT_CSV)
+		opts_out->delim = ",";
+	else if (opts_out->format == COPY_FORMAT_TEXT)
+		opts_out->delim = "\t";
 
-	if (!opts_out->null_print)
-		opts_out->null_print = opts_out->format == COPY_FORMAT_CSV ? "" : "\\N";
-	opts_out->null_print_len = strlen(opts_out->null_print);
+	if (opts_out->null_print)
+	{
+		if (strchr(opts_out->null_print, '\r') != NULL ||
+			strchr(opts_out->null_print, '\n') != NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("COPY null representation cannot use newline or carriage return")));
+
+	}
+	else if (opts_out->format == COPY_FORMAT_CSV)
+		opts_out->null_print = "";
+	else if (opts_out->format == COPY_FORMAT_TEXT)
+		opts_out->null_print = "\\N";
+
+	if (opts_out->null_print)
+		opts_out->null_print_len = strlen(opts_out->null_print);
 
 	if (opts_out->format == COPY_FORMAT_CSV)
 	{
@@ -707,25 +752,6 @@ ProcessCopyOptions(ParseState *pstate,
 			opts_out->escape = opts_out->quote;
 	}
 
-	/* Only single-byte delimiter strings are supported. */
-	if (strlen(opts_out->delim) != 1)
-		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("COPY delimiter must be a single one-byte character")));
-
-	/* Disallow end-of-line characters */
-	if (strchr(opts_out->delim, '\r') != NULL ||
-		strchr(opts_out->delim, '\n') != NULL)
-		ereport(ERROR,
-				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				 errmsg("COPY delimiter cannot be newline or carriage return")));
-
-	if (strchr(opts_out->null_print, '\r') != NULL ||
-		strchr(opts_out->null_print, '\n') != NULL)
-		ereport(ERROR,
-				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				 errmsg("COPY null representation cannot use newline or carriage return")));
-
 	if (opts_out->default_print)
 	{
 		opts_out->default_print_len = strlen(opts_out->default_print);
@@ -738,7 +764,7 @@ ProcessCopyOptions(ParseState *pstate,
 	}
 
 	/*
-	 * Disallow unsafe delimiter characters in non-CSV mode.  We can't allow
+	 * Disallow unsafe delimiter characters in text mode.  We can't allow
 	 * backslash because it would be ambiguous.  We can't allow the other
 	 * cases because data characters matching the delimiter must be
 	 * backslashed, and certain backslash combinations are interpreted
@@ -747,7 +773,7 @@ ProcessCopyOptions(ParseState *pstate,
 	 * future-proofing.  Likewise we disallow all digits though only octal
 	 * digits are actually dangerous.
 	 */
-	if (opts_out->format != COPY_FORMAT_CSV &&
+	if (opts_out->format == COPY_FORMAT_TEXT &&
 		strchr("\\.abcdefghijklmnopqrstuvwxyz0123456789",
 			   opts_out->delim[0]) != NULL)
 		ereport(ERROR,
@@ -761,6 +787,12 @@ ProcessCopyOptions(ParseState *pstate,
 		/*- translator: %s is the name of a COPY option, e.g. ON_ERROR */
 				 errmsg("cannot specify %s in BINARY mode", "HEADER")));
 
+	if (opts_out->format == COPY_FORMAT_RAW && opts_out->header_line)
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+		/*- translator: %s is the name of a COPY option, e.g. ON_ERROR */
+				 errmsg("cannot specify %s in RAW mode", "HEADER")));
+
 	/* Check quote */
 	if (opts_out->format != COPY_FORMAT_CSV && opts_out->quote != NULL)
 		ereport(ERROR,
@@ -839,7 +871,8 @@ ProcessCopyOptions(ParseState *pstate,
 						"COPY TO")));
 
 	/* Don't allow the delimiter to appear in the null string. */
-	if (strchr(opts_out->null_print, opts_out->delim[0]) != NULL)
+	if (opts_out->delim && opts_out->null_print &&
+		strchr(opts_out->null_print, opts_out->delim[0]) != NULL)
 		ereport(ERROR,
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 		/*- translator: %s is the name of a COPY option, e.g. NULL */
@@ -875,7 +908,7 @@ ProcessCopyOptions(ParseState *pstate,
 							"COPY TO")));
 
 		/* Don't allow the delimiter to appear in the default string. */
-		if (strchr(opts_out->default_print, opts_out->delim[0]) != NULL)
+		if (opts_out->delim && strchr(opts_out->default_print, opts_out->delim[0]) != NULL)
 			ereport(ERROR,
 					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 			/*- translator: %s is the name of a COPY option, e.g. NULL */
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index f350a4ff97..73a3f38d90 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -1438,6 +1438,13 @@ BeginCopyFrom(ParseState *pstate,
 	/* Generate or convert list of attributes to process */
 	cstate->attnumlist = CopyGetAttnums(tupDesc, cstate->rel, attnamelist);
 
+	/* Enforce single column requirement for RAW format */
+	if (cstate->opts.format == COPY_FORMAT_RAW &&
+		list_length(cstate->attnumlist) != 1)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("COPY with format 'raw' must specify exactly one column")));
+
 	num_phys_attrs = tupDesc->natts;
 
 	/* Convert FORCE_NOT_NULL name list to per-column flags, check validity */
diff --git a/src/backend/commands/copyfromparse.c b/src/backend/commands/copyfromparse.c
index 51eb14d743..5f17c41350 100644
--- a/src/backend/commands/copyfromparse.c
+++ b/src/backend/commands/copyfromparse.c
@@ -7,7 +7,7 @@
  * formats.  The main entry point is NextCopyFrom(), which parses the
  * next input line and returns it as Datums.
  *
- * In text/CSV mode, the parsing happens in multiple stages:
+ * In text/CSV/raw mode, the parsing happens in multiple stages:
  *
  * [data source] --> raw_buf --> input_buf --> line_buf --> attribute_buf
  *                1.          2.            3.           4.
@@ -25,7 +25,7 @@
  *    is copied into 'line_buf', with quotes and escape characters still
  *    intact.
  *
- * 4. CopyReadAttributesText/CSV() function takes the input line from
+ * 4. CopyReadAttributesText/CSV/Raw() function takes the input line from
  *    'line_buf', and splits it into fields, unescaping the data as required.
  *    The fields are stored in 'attribute_buf', and 'raw_fields' array holds
  *    pointers to each field.
@@ -731,7 +731,7 @@ CopyReadBinaryData(CopyFromState cstate, char *dest, int nbytes)
 }
 
 /*
- * Read raw fields in the next line for COPY FROM in text or csv mode.
+ * Read raw fields in the next line for COPY FROM in text, csv, or raw mode.
  * Return false if no more lines.
  *
  * An internal temporary buffer is returned via 'fields'. It is valid until
@@ -747,7 +747,7 @@ NextCopyFromRawFields(CopyFromState cstate, char ***fields, int *nfields)
 	int			fldct;
 	bool		done;
 
-	/* only available for text or csv input */
+	/* only available for text, csv, or raw input */
 	Assert(cstate->opts.format != COPY_FORMAT_BINARY);
 
 	/* on input check that the header line is correct if needed */
@@ -765,6 +765,9 @@ NextCopyFromRawFields(CopyFromState cstate, char ***fields, int *nfields)
 		{
 			int			fldnum;
 
+			Assert(cstate->opts.format == COPY_FORMAT_CSV ||
+				   cstate->opts.format == COPY_FORMAT_TEXT);
+
 			if (cstate->opts.format == COPY_FORMAT_CSV)
 				fldct = CopyReadAttributesCSV(cstate);
 			else
@@ -822,8 +825,16 @@ NextCopyFromRawFields(CopyFromState cstate, char ***fields, int *nfields)
 	/* Parse the line into de-escaped field values */
 	if (cstate->opts.format == COPY_FORMAT_CSV)
 		fldct = CopyReadAttributesCSV(cstate);
-	else
+	else if (cstate->opts.format == COPY_FORMAT_TEXT)
 		fldct = CopyReadAttributesText(cstate);
+	else
+	{
+		Assert(cstate->opts.format == COPY_FORMAT_RAW);
+		Assert(cstate->max_fields == 1);
+		/* Point raw_fields directly to line_buf data */
+		cstate->raw_fields[0] = cstate->line_buf.data;
+		fldct = 1;
+	}
 
 	*fields = cstate->raw_fields;
 	*nfields = fldct;
@@ -1146,6 +1157,22 @@ CopyReadLine(CopyFromState cstate)
 				cstate->line_buf.len -= 2;
 				cstate->line_buf.data[cstate->line_buf.len] = '\0';
 				break;
+			case EOL_CUSTOM:
+				{
+					int			delim_len;
+
+					Assert(cstate->opts.format == COPY_FORMAT_RAW);
+					Assert(cstate->opts.delim);
+					delim_len = strlen(cstate->opts.delim);
+					Assert(delim_len > 0);
+					Assert(cstate->line_buf.len >= delim_len);
+					Assert(memcmp(cstate->line_buf.data + cstate->line_buf.len - delim_len,
+								  cstate->opts.delim,
+								  delim_len) == 0);
+					cstate->line_buf.len -= delim_len;
+					cstate->line_buf.data[cstate->line_buf.len] = '\0';
+				}
+				break;
 			case EOL_UNKNOWN:
 				/* shouldn't get here */
 				Assert(false);
@@ -1178,6 +1205,9 @@ CopyReadLineText(CopyFromState cstate)
 	char		quotec = '\0';
 	char		escapec = '\0';
 
+	/* Raw text variables */
+	int			delim_len = 0;
+
 	if (cstate->opts.format == COPY_FORMAT_CSV)
 	{
 		quotec = cstate->opts.quote[0];
@@ -1186,6 +1216,10 @@ CopyReadLineText(CopyFromState cstate)
 		if (quotec == escapec)
 			escapec = '\0';
 	}
+	else if (cstate->opts.format == COPY_FORMAT_RAW)
+	{
+		delim_len = cstate->opts.delim ? strlen(cstate->opts.delim) : 0;
+	}
 
 	/*
 	 * The objective of this loop is to transfer the entire next input line
@@ -1211,6 +1245,10 @@ CopyReadLineText(CopyFromState cstate)
 	 *
 	 * For a little extra speed within the loop, we copy input_buf and
 	 * input_buf_len into local variables.
+	 *
+	 * In raw text mode, we treat the input data as-is, without any parsing,
+	 * quoting, or escaping. We are only interested in locating the delimiter
+	 * to determine the boundaries of each data value.
 	 */
 	copy_input_buf = cstate->input_buf;
 	input_buf_ptr = cstate->input_buf_index;
@@ -1253,205 +1291,242 @@ CopyReadLineText(CopyFromState cstate)
 
 		/* OK to fetch a character */
 		prev_raw_ptr = input_buf_ptr;
-		c = copy_input_buf[input_buf_ptr++];
 
-		if (cstate->opts.format == COPY_FORMAT_CSV)
+		if (cstate->opts.format == COPY_FORMAT_RAW)
 		{
-			/*
-			 * If character is '\r', we may need to look ahead below.  Force
-			 * fetch of the next character if we don't already have it.  We
-			 * need to do this before changing CSV state, in case '\r' is also
-			 * the quote or escape character.
-			 */
-			if (c == '\r')
+			if (delim_len == 0)
 			{
-				IF_NEED_REFILL_AND_NOT_EOF_CONTINUE(0);
+				/* When reading entire file, consume all remaining bytes at once */
+				input_buf_ptr = copy_buf_len;
+				continue;
 			}
-
-			/*
-			 * Dealing with quotes and escapes here is mildly tricky. If the
-			 * quote char is also the escape char, there's no problem - we
-			 * just use the char as a toggle. If they are different, we need
-			 * to ensure that we only take account of an escape inside a
-			 * quoted field and immediately preceding a quote char, and not
-			 * the second in an escape-escape sequence.
-			 */
-			if (in_quote && c == escapec)
-				last_was_esc = !last_was_esc;
-			if (c == quotec && !last_was_esc)
-				in_quote = !in_quote;
-			if (c != escapec)
-				last_was_esc = false;
-
-			/*
-			 * Updating the line count for embedded CR and/or LF chars is
-			 * necessarily a little fragile - this test is probably about the
-			 * best we can do.  (XXX it's arguable whether we should do this
-			 * at all --- is cur_lineno a physical or logical count?)
-			 */
-			if (in_quote && c == (cstate->eol_type == EOL_NL ? '\n' : '\r'))
-				cstate->cur_lineno++;
-		}
-
-		/* Process \r */
-		if (c == '\r' && (cstate->opts.format != COPY_FORMAT_CSV || !in_quote))
-		{
-			/* Check for \r\n on first line, _and_ handle \r\n. */
-			if (cstate->eol_type == EOL_UNKNOWN ||
-				cstate->eol_type == EOL_CRNL)
+			else
 			{
-				/*
-				 * If need more data, go back to loop top to load it.
-				 *
-				 * Note that if we are at EOF, c will wind up as '\0' because
-				 * of the guaranteed pad of input_buf.
-				 */
-				IF_NEED_REFILL_AND_NOT_EOF_CONTINUE(0);
+				char	   *delim_pos;
 
-				/* get next char */
-				c = copy_input_buf[input_buf_ptr];
+				/* Check for delimiter, possibly multi-byte */
+				IF_NEED_REFILL_AND_NOT_EOF_CONTINUE(delim_len - 1);
 
-				if (c == '\n')
+				/* Look for delimiter in the remaining buffer */
+				delim_pos = strstr(&copy_input_buf[input_buf_ptr], cstate->opts.delim);
+				if (delim_pos != NULL)
 				{
-					input_buf_ptr++;	/* eat newline */
-					cstate->eol_type = EOL_CRNL;	/* in case not set yet */
+					/* Found delimiter - move pointer to its position */
+					input_buf_ptr = delim_pos - copy_input_buf;
+					cstate->eol_type = EOL_CUSTOM;
+					input_buf_ptr += delim_len;
+					break;
 				}
 				else
 				{
-					/* found \r, but no \n */
+					/* No delimiter found - move to end of current buffer */
+					input_buf_ptr = copy_buf_len;
+					continue;
+				}
+			}
+		}
+		else
+		{
+			c = copy_input_buf[input_buf_ptr++];
+
+			if (cstate->opts.format == COPY_FORMAT_CSV)
+			{
+				/*
+				 * If character is '\r', we may need to look ahead below.  Force
+				 * fetch of the next character if we don't already have it.  We
+				 * need to do this before changing CSV state, in case '\r' is also
+				 * the quote or escape character.
+				 */
+				if (c == '\r')
+				{
+					IF_NEED_REFILL_AND_NOT_EOF_CONTINUE(0);
+				}
+
+				/*
+				 * Dealing with quotes and escapes here is mildly tricky. If the
+				 * quote char is also the escape char, there's no problem - we
+				 * just use the char as a toggle. If they are different, we need
+				 * to ensure that we only take account of an escape inside a
+				 * quoted field and immediately preceding a quote char, and not
+				 * the second in an escape-escape sequence.
+				 */
+				if (in_quote && c == escapec)
+					last_was_esc = !last_was_esc;
+				if (c == quotec && !last_was_esc)
+					in_quote = !in_quote;
+				if (c != escapec)
+					last_was_esc = false;
+
+				/*
+				 * Updating the line count for embedded CR and/or LF chars is
+				 * necessarily a little fragile - this test is probably about the
+				 * best we can do.  (XXX it's arguable whether we should do this
+				 * at all --- is cur_lineno a physical or logical count?)
+				 */
+				if (in_quote && c == (cstate->eol_type == EOL_NL ? '\n' : '\r'))
+					cstate->cur_lineno++;
+			}
+
+			/* Process \r */
+			if (c == '\r' && (cstate->opts.format != COPY_FORMAT_CSV || !in_quote))
+			{
+				/* Check for \r\n on first line, _and_ handle \r\n. */
+				if (cstate->eol_type == EOL_UNKNOWN ||
+					cstate->eol_type == EOL_CRNL)
+				{
+					/*
+					 * If need more data, go back to loop top to load it.
+					 *
+					 * Note that if we are at EOF, c will wind up as '\0' because
+					 * of the guaranteed pad of input_buf.
+					 */
+					IF_NEED_REFILL_AND_NOT_EOF_CONTINUE(0);
+
+					/* get next char */
+					c = copy_input_buf[input_buf_ptr];
+
+					if (c == '\n')
+					{
+						input_buf_ptr++;	/* eat newline */
+						cstate->eol_type = EOL_CRNL;	/* in case not set yet */
+					}
+					else
+					{
+						/* found \r, but no \n */
+						if (cstate->eol_type == EOL_CRNL)
+							ereport(ERROR,
+									(errcode(ERRCODE_BAD_COPY_FILE_FORMAT),
+									 cstate->opts.format != COPY_FORMAT_CSV ?
+									 errmsg("literal carriage return found in data") :
+									 errmsg("unquoted carriage return found in data"),
+									 cstate->opts.format != COPY_FORMAT_CSV ?
+									 errhint("Use \"\\r\" to represent carriage return.") :
+									 errhint("Use quoted CSV field to represent carriage return.")));
+
+						/*
+						 * if we got here, it is the first line and we didn't find
+						 * \n, so don't consume the peeked character
+						 */
+						cstate->eol_type = EOL_CR;
+					}
+				}
+				else if (cstate->eol_type == EOL_NL)
+					ereport(ERROR,
+							(errcode(ERRCODE_BAD_COPY_FILE_FORMAT),
+							 cstate->opts.format != COPY_FORMAT_CSV ?
+							 errmsg("literal carriage return found in data") :
+							 errmsg("unquoted carriage return found in data"),
+							 cstate->opts.format != COPY_FORMAT_CSV ?
+							 errhint("Use \"\\r\" to represent carriage return.") :
+							 errhint("Use quoted CSV field to represent carriage return.")));
+				/* If reach here, we have found the line terminator */
+				break;
+			}
+
+			/* Process \n */
+			if (c == '\n' && (cstate->opts.format != COPY_FORMAT_CSV || !in_quote))
+			{
+				if (cstate->eol_type == EOL_CR || cstate->eol_type == EOL_CRNL)
+					ereport(ERROR,
+							(errcode(ERRCODE_BAD_COPY_FILE_FORMAT),
+							 cstate->opts.format != COPY_FORMAT_CSV ?
+							 errmsg("literal newline found in data") :
+							 errmsg("unquoted newline found in data"),
+							 cstate->opts.format != COPY_FORMAT_CSV ?
+							 errhint("Use \"\\n\" to represent newline.") :
+							 errhint("Use quoted CSV field to represent newline.")));
+				cstate->eol_type = EOL_NL;	/* in case not set yet */
+				/* If reach here, we have found the line terminator */
+				break;
+			}
+
+			/*
+			 * Process backslash, except in CSV mode where backslash is a normal
+			 * character.
+			 */
+			if (c == '\\' && cstate->opts.format != COPY_FORMAT_CSV)
+			{
+				char		c2;
+
+				IF_NEED_REFILL_AND_NOT_EOF_CONTINUE(0);
+				IF_NEED_REFILL_AND_EOF_BREAK(0);
+
+				/* -----
+				 * get next character
+				 * Note: we do not change c so if it isn't \., we can fall
+				 * through and continue processing.
+				 * -----
+				 */
+				c2 = copy_input_buf[input_buf_ptr];
+
+				if (c2 == '.')
+				{
+					input_buf_ptr++;	/* consume the '.' */
 					if (cstate->eol_type == EOL_CRNL)
-						ereport(ERROR,
-								(errcode(ERRCODE_BAD_COPY_FILE_FORMAT),
-								 cstate->opts.format != COPY_FORMAT_CSV ?
-								 errmsg("literal carriage return found in data") :
-								 errmsg("unquoted carriage return found in data"),
-								 cstate->opts.format != COPY_FORMAT_CSV ?
-								 errhint("Use \"\\r\" to represent carriage return.") :
-								 errhint("Use quoted CSV field to represent carriage return.")));
+					{
+						/* Get the next character */
+						IF_NEED_REFILL_AND_NOT_EOF_CONTINUE(0);
+						/* if hit_eof, c2 will become '\0' */
+						c2 = copy_input_buf[input_buf_ptr++];
 
-					/*
-					 * if we got here, it is the first line and we didn't find
-					 * \n, so don't consume the peeked character
-					 */
-					cstate->eol_type = EOL_CR;
-				}
-			}
-			else if (cstate->eol_type == EOL_NL)
-				ereport(ERROR,
-						(errcode(ERRCODE_BAD_COPY_FILE_FORMAT),
-						 cstate->opts.format != COPY_FORMAT_CSV ?
-						 errmsg("literal carriage return found in data") :
-						 errmsg("unquoted carriage return found in data"),
-						 cstate->opts.format != COPY_FORMAT_CSV ?
-						 errhint("Use \"\\r\" to represent carriage return.") :
-						 errhint("Use quoted CSV field to represent carriage return.")));
-			/* If reach here, we have found the line terminator */
-			break;
-		}
+						if (c2 == '\n')
+							ereport(ERROR,
+									(errcode(ERRCODE_BAD_COPY_FILE_FORMAT),
+									 errmsg("end-of-copy marker does not match previous newline style")));
+						else if (c2 != '\r')
+							ereport(ERROR,
+									(errcode(ERRCODE_BAD_COPY_FILE_FORMAT),
+									 errmsg("end-of-copy marker is not alone on its line")));
+					}
 
-		/* Process \n */
-		if (c == '\n' && (cstate->opts.format != COPY_FORMAT_CSV || !in_quote))
-		{
-			if (cstate->eol_type == EOL_CR || cstate->eol_type == EOL_CRNL)
-				ereport(ERROR,
-						(errcode(ERRCODE_BAD_COPY_FILE_FORMAT),
-						 cstate->opts.format != COPY_FORMAT_CSV ?
-						 errmsg("literal newline found in data") :
-						 errmsg("unquoted newline found in data"),
-						 cstate->opts.format != COPY_FORMAT_CSV ?
-						 errhint("Use \"\\n\" to represent newline.") :
-						 errhint("Use quoted CSV field to represent newline.")));
-			cstate->eol_type = EOL_NL;	/* in case not set yet */
-			/* If reach here, we have found the line terminator */
-			break;
-		}
-
-		/*
-		 * Process backslash, except in CSV mode where backslash is a normal
-		 * character.
-		 */
-		if (c == '\\' && cstate->opts.format != COPY_FORMAT_CSV)
-		{
-			char		c2;
-
-			IF_NEED_REFILL_AND_NOT_EOF_CONTINUE(0);
-			IF_NEED_REFILL_AND_EOF_BREAK(0);
-
-			/* -----
-			 * get next character
-			 * Note: we do not change c so if it isn't \., we can fall
-			 * through and continue processing.
-			 * -----
-			 */
-			c2 = copy_input_buf[input_buf_ptr];
-
-			if (c2 == '.')
-			{
-				input_buf_ptr++;	/* consume the '.' */
-				if (cstate->eol_type == EOL_CRNL)
-				{
 					/* Get the next character */
 					IF_NEED_REFILL_AND_NOT_EOF_CONTINUE(0);
 					/* if hit_eof, c2 will become '\0' */
 					c2 = copy_input_buf[input_buf_ptr++];
 
-					if (c2 == '\n')
+					if (c2 != '\r' && c2 != '\n')
+						ereport(ERROR,
+								(errcode(ERRCODE_BAD_COPY_FILE_FORMAT),
+								 errmsg("end-of-copy marker is not alone on its line")));
+
+					if ((cstate->eol_type == EOL_NL && c2 != '\n') ||
+						(cstate->eol_type == EOL_CRNL && c2 != '\n') ||
+						(cstate->eol_type == EOL_CR && c2 != '\r'))
 						ereport(ERROR,
 								(errcode(ERRCODE_BAD_COPY_FILE_FORMAT),
 								 errmsg("end-of-copy marker does not match previous newline style")));
-					else if (c2 != '\r')
+
+					/*
+					 * If there is any data on this line before the \., complain.
+					 */
+					if (cstate->line_buf.len > 0 ||
+						prev_raw_ptr > cstate->input_buf_index)
 						ereport(ERROR,
 								(errcode(ERRCODE_BAD_COPY_FILE_FORMAT),
 								 errmsg("end-of-copy marker is not alone on its line")));
-				}
-
-				/* Get the next character */
-				IF_NEED_REFILL_AND_NOT_EOF_CONTINUE(0);
-				/* if hit_eof, c2 will become '\0' */
-				c2 = copy_input_buf[input_buf_ptr++];
-
-				if (c2 != '\r' && c2 != '\n')
-					ereport(ERROR,
-							(errcode(ERRCODE_BAD_COPY_FILE_FORMAT),
-							 errmsg("end-of-copy marker is not alone on its line")));
-
-				if ((cstate->eol_type == EOL_NL && c2 != '\n') ||
-					(cstate->eol_type == EOL_CRNL && c2 != '\n') ||
-					(cstate->eol_type == EOL_CR && c2 != '\r'))
-					ereport(ERROR,
-							(errcode(ERRCODE_BAD_COPY_FILE_FORMAT),
-							 errmsg("end-of-copy marker does not match previous newline style")));
 
-				/*
-				 * If there is any data on this line before the \., complain.
-				 */
-				if (cstate->line_buf.len > 0 ||
-					prev_raw_ptr > cstate->input_buf_index)
-					ereport(ERROR,
-							(errcode(ERRCODE_BAD_COPY_FILE_FORMAT),
-							 errmsg("end-of-copy marker is not alone on its line")));
-
-				/*
-				 * Discard the \. and newline, then report EOF.
-				 */
-				cstate->input_buf_index = input_buf_ptr;
-				result = true;	/* report EOF */
-				break;
-			}
-			else
-			{
-				/*
-				 * If we are here, it means we found a backslash followed by
-				 * something other than a period.  In non-CSV mode, anything
-				 * after a backslash is special, so we skip over that second
-				 * character too.  If we didn't do that \\. would be
-				 * considered an eof-of copy, while in non-CSV mode it is a
-				 * literal backslash followed by a period.
-				 */
-				input_buf_ptr++;
+					/*
+					 * Discard the \. and newline, then report EOF.
+					 */
+					cstate->input_buf_index = input_buf_ptr;
+					result = true;	/* report EOF */
+					break;
+				}
+				else
+				{
+					/*
+					 * If we are here, it means we found a backslash followed by
+					 * something other than a period.  In non-CSV mode, anything
+					 * after a backslash is special, so we skip over that second
+					 * character too.  If we didn't do that \\. would be
+					 * considered an eof-of copy, while in non-CSV mode it is a
+					 * literal backslash followed by a period.
+					 */
+					input_buf_ptr++;
+				}
 			}
-		}
-	}							/* end of outer loop */
+		}							/* end of else (non-raw text mode) */
+	}								/* end of outer loop */
 
 	/*
 	 * Transfer any still-uncopied data to line_buf.
@@ -1461,6 +1536,7 @@ CopyReadLineText(CopyFromState cstate)
 	return result;
 }
 
+
 /*
  *	Return decimal value for a hexadecimal digit
  */
@@ -1937,7 +2013,6 @@ endfield:
 	return fieldno;
 }
 
-
 /*
  * Read a binary attribute
  */
diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index 03c9d71d34..2611c0a360 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -113,6 +113,7 @@ static void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 static void CopyAttributeOutText(CopyToState cstate, const char *string);
 static void CopyAttributeOutCSV(CopyToState cstate, const char *string,
 								bool use_quote);
+static void CopyAttributeOutRaw(CopyToState cstate, const char *string);
 
 /* Low-level communications functions */
 static void SendCopyBegin(CopyToState cstate);
@@ -191,7 +192,14 @@ CopySendEndOfRow(CopyToState cstate)
 	switch (cstate->copy_dest)
 	{
 		case COPY_FILE:
-			if (cstate->opts.format != COPY_FORMAT_BINARY)
+			if (cstate->opts.format == COPY_FORMAT_RAW &&
+				cstate->opts.delim != NULL)
+			{
+				/* Output the user-specified delimiter between rows */
+				CopySendString(cstate, cstate->opts.delim);
+			}
+			else if (cstate->opts.format == COPY_FORMAT_TEXT ||
+					 cstate->opts.format == COPY_FORMAT_CSV)
 			{
 				/* Default line termination depends on platform */
 #ifndef WIN32
@@ -235,9 +243,18 @@ CopySendEndOfRow(CopyToState cstate)
 			}
 			break;
 		case COPY_FRONTEND:
-			/* The FE/BE protocol uses \n as newline for all platforms */
-			if (cstate->opts.format != COPY_FORMAT_BINARY)
+			if (cstate->opts.format == COPY_FORMAT_RAW &&
+				cstate->opts.delim != NULL)
+			{
+				/* Output the user-specified delimiter between rows */
+				CopySendString(cstate, cstate->opts.delim);
+			}
+			else if (cstate->opts.format == COPY_FORMAT_TEXT ||
+					 cstate->opts.format == COPY_FORMAT_CSV)
+			{
+				/* The FE/BE protocol uses \n as newline for all platforms */
 				CopySendChar(cstate, '\n');
+			}
 
 			/* Dump the accumulated row as one CopyData message */
 			(void) pq_putmessage(PqMsg_CopyData, fe_msgbuf->data, fe_msgbuf->len);
@@ -574,6 +591,13 @@ BeginCopyTo(ParseState *pstate,
 	/* Generate or convert list of attributes to process */
 	cstate->attnumlist = CopyGetAttnums(tupDesc, cstate->rel, attnamelist);
 
+	/* Enforce single column requirement for RAW format */
+	if (cstate->opts.format == COPY_FORMAT_RAW &&
+		list_length(cstate->attnumlist) != 1)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("COPY with format 'raw' must specify exactly one column")));
+
 	num_phys_attrs = tupDesc->natts;
 
 	/* Convert FORCE_QUOTE name list to per-column flags, check validity */
@@ -839,8 +863,10 @@ DoCopyTo(CopyToState cstate)
 
 				if (cstate->opts.format == COPY_FORMAT_CSV)
 					CopyAttributeOutCSV(cstate, colname, false);
-				else
+				else if (cstate->opts.format == COPY_FORMAT_TEXT)
 					CopyAttributeOutText(cstate, colname);
+				else if (cstate->opts.format == COPY_FORMAT_RAW)
+					CopyAttributeOutRaw(cstate, colname);
 			}
 
 			CopySendEndOfRow(cstate);
@@ -921,7 +947,8 @@ CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot)
 	/* Make sure the tuple is fully deconstructed */
 	slot_getallattrs(slot);
 
-	if (cstate->opts.format != COPY_FORMAT_BINARY)
+	if (cstate->opts.format == COPY_FORMAT_TEXT ||
+		cstate->opts.format == COPY_FORMAT_CSV)
 	{
 		bool		need_delim = false;
 
@@ -949,7 +976,7 @@ CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot)
 			}
 		}
 	}
-	else
+	else if (cstate->opts.format == COPY_FORMAT_BINARY)
 	{
 		foreach_int(attnum, cstate->attnumlist)
 		{
@@ -969,6 +996,35 @@ CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot)
 			}
 		}
 	}
+	else if (cstate->opts.format == COPY_FORMAT_RAW)
+	{
+		int			attnum;
+		Datum		value;
+		bool		isnull;
+
+		/* Assert only one column is being copied */
+		Assert(list_length(cstate->attnumlist) == 1);
+
+		attnum = linitial_int(cstate->attnumlist);
+		value = slot->tts_values[attnum - 1];
+		isnull = slot->tts_isnull[attnum - 1];
+
+		if (!isnull)
+		{
+			char	   *string = OutputFunctionCall(&out_functions[attnum - 1],
+													value);
+
+			CopyAttributeOutRaw(cstate, string);
+		}
+		/* For RAW format, we don't send anything for NULL values */
+	}
+	else
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("Unsupported COPY format")));
+	}
+
 
 	CopySendEndOfRow(cstate);
 
@@ -1223,6 +1279,28 @@ CopyAttributeOutCSV(CopyToState cstate, const char *string,
 	}
 }
 
+/*
+ * Send text representation of one attribute for RAW format.
+ */
+static void
+CopyAttributeOutRaw(CopyToState cstate, const char *string)
+{
+	const char *ptr;
+
+	/* Ensure the format is RAW */
+	Assert(cstate->opts.format == COPY_FORMAT_RAW);
+
+	/* Ensure exactly one column is being processed */
+	Assert(list_length(cstate->attnumlist) == 1);
+
+	if (cstate->need_transcoding)
+		ptr = pg_server_to_any(string, strlen(string), cstate->file_encoding);
+	else
+		ptr = string;
+
+	CopySendString(cstate, ptr);
+}
+
 /*
  * copy_dest_startup --- executor startup
  */
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 1be0056af7..7f8d6f4f94 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -3239,7 +3239,7 @@ match_previous_words(int pattern_id,
 
 	/* Complete COPY <sth> FROM|TO filename WITH (FORMAT */
 	else if (Matches("COPY|\\copy", MatchAny, "FROM|TO", MatchAny, "WITH", "(", "FORMAT"))
-		COMPLETE_WITH("binary", "csv", "text");
+		COMPLETE_WITH("binary", "csv", "text", "raw");
 
 	/* Complete COPY <sth> FROM filename WITH (ON_ERROR */
 	else if (Matches("COPY|\\copy", MatchAny, "FROM|TO", MatchAny, "WITH", "(", "ON_ERROR"))
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index c3d1df267f..8996bc89e5 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -59,6 +59,7 @@ typedef enum CopyFormat
 	COPY_FORMAT_TEXT = 0,
 	COPY_FORMAT_BINARY,
 	COPY_FORMAT_CSV,
+	COPY_FORMAT_RAW,
 } CopyFormat;
 
 /*
@@ -79,7 +80,7 @@ typedef struct CopyFormatOptions
 	char	   *null_print_client;	/* same converted to file encoding */
 	char	   *default_print;	/* DEFAULT marker string */
 	int			default_print_len;	/* length of same */
-	char	   *delim;			/* column delimiter (must be 1 byte) */
+	char	   *delim;			/* delimiter (1 byte, except for raw format) */
 	char	   *quote;			/* CSV quote char (must be 1 byte) */
 	char	   *escape;			/* CSV escape char (must be 1 byte) */
 	List	   *force_quote;	/* list of column names */
diff --git a/src/include/commands/copyfrom_internal.h b/src/include/commands/copyfrom_internal.h
index cad52fcc78..b8693ae59e 100644
--- a/src/include/commands/copyfrom_internal.h
+++ b/src/include/commands/copyfrom_internal.h
@@ -38,6 +38,7 @@ typedef enum EolType
 	EOL_NL,
 	EOL_CR,
 	EOL_CRNL,
+	EOL_CUSTOM,
 } EolType;
 
 /*
diff --git a/src/test/regress/expected/copy.out b/src/test/regress/expected/copy.out
index f554d42c84..2825d833ea 100644
--- a/src/test/regress/expected/copy.out
+++ b/src/test/regress/expected/copy.out
@@ -325,3 +325,55 @@ SELECT tableoid::regclass, id % 2 = 0 is_even, count(*) from parted_si GROUP BY
 (2 rows)
 
 DROP TABLE parted_si;
+-- Test COPY FORMAT raw
+\set filename :abs_srcdir '/data/emp.data'
+CREATE TABLE copy_raw_test (col text);
+COPY copy_raw_test FROM :'filename' (FORMAT raw);
+SELECT col FROM copy_raw_test;
+                  col                   
+----------------------------------------
+ sharon  25      (15,12) 1000    sam   +
+ sam     30      (10,5)  2000    bill  +
+ bill    20      (11,10) 1000    sharon+
+ 
+(1 row)
+
+TRUNCATE copy_raw_test;
+COPY copy_raw_test FROM :'filename' (FORMAT raw, DELIMITER E'\n');
+SELECT col FROM copy_raw_test ORDER BY col COLLATE "C";
+                  col                   
+----------------------------------------
+ bill    20      (11,10) 1000    sharon
+ sam     30      (10,5)  2000    bill
+ sharon  25      (15,12) 1000    sam
+(3 rows)
+
+COPY copy_raw_test TO stdout (FORMAT raw, DELIMITER E'\n***\n');
+sharon	25	(15,12)	1000	sam
+***
+sam	30	(10,5)	2000	bill
+***
+bill	20	(11,10)	1000	sharon
+***
+\qecho
+
+TRUNCATE copy_raw_test;
+COPY copy_raw_test FROM stdin (FORMAT raw, DELIMITER E'\n***\n');
+SELECT col FROM copy_raw_test ORDER BY col COLLATE "C";
+  col   
+--------
+ 
+ "def",
+ abc\.
+ ghi
+(4 rows)
+
+COPY copy_raw_test TO stdout (FORMAT raw, DELIMITER E'\n***\n');
+abc\.
+***
+"def",
+***
+
+***
+ghi
+***
diff --git a/src/test/regress/expected/copy2.out b/src/test/regress/expected/copy2.out
index 64ea33aeae..f31bd6a322 100644
--- a/src/test/regress/expected/copy2.out
+++ b/src/test/regress/expected/copy2.out
@@ -90,15 +90,35 @@ COPY x from stdin (format BINARY, delimiter ',');
 ERROR:  cannot specify DELIMITER in BINARY mode
 COPY x from stdin (format BINARY, null 'x');
 ERROR:  cannot specify NULL in BINARY mode
+COPY x from stdin (format RAW, null 'x');
+ERROR:  cannot specify NULL in RAW mode
+COPY x from stdin (format TEXT, escape 'x');
+ERROR:  COPY ESCAPE requires CSV mode
+COPY x from stdin (format BINARY, escape 'x');
+ERROR:  COPY ESCAPE requires CSV mode
+COPY x from stdin (format RAW, escape 'x');
+ERROR:  COPY ESCAPE requires CSV mode
+COPY x from stdin (format TEXT, quote 'x');
+ERROR:  COPY QUOTE requires CSV mode
+COPY x from stdin (format BINARY, quote 'x');
+ERROR:  COPY QUOTE requires CSV mode
+COPY x from stdin (format RAW, quote 'x');
+ERROR:  COPY QUOTE requires CSV mode
+COPY x from stdin (format RAW, header);
+ERROR:  cannot specify HEADER in RAW mode
 COPY x from stdin (format BINARY, on_error ignore);
 ERROR:  only ON_ERROR STOP is allowed in BINARY mode
 COPY x from stdin (on_error unsupported);
 ERROR:  COPY ON_ERROR "unsupported" not recognized
 LINE 1: COPY x from stdin (on_error unsupported);
                            ^
-COPY x from stdin (format TEXT, force_quote(a));
+COPY x to stdout (format TEXT, force_quote(a));
 ERROR:  COPY FORCE_QUOTE requires CSV mode
-COPY x from stdin (format TEXT, force_quote *);
+COPY x to stdout (format TEXT, force_quote *);
+ERROR:  COPY FORCE_QUOTE requires CSV mode
+COPY x to stdout (format RAW, force_quote(a));
+ERROR:  COPY FORCE_QUOTE requires CSV mode
+COPY x to stdout (format RAW, force_quote *);
 ERROR:  COPY FORCE_QUOTE requires CSV mode
 COPY x from stdin (format CSV, force_quote(a));
 ERROR:  COPY FORCE_QUOTE cannot be used with COPY FROM
@@ -108,6 +128,10 @@ COPY x from stdin (format TEXT, force_not_null(a));
 ERROR:  COPY FORCE_NOT_NULL requires CSV mode
 COPY x from stdin (format TEXT, force_not_null *);
 ERROR:  COPY FORCE_NOT_NULL requires CSV mode
+COPY x from stdin (format RAW, force_not_null(a));
+ERROR:  COPY FORCE_NOT_NULL requires CSV mode
+COPY x from stdin (format RAW, force_not_null *);
+ERROR:  COPY FORCE_NOT_NULL requires CSV mode
 COPY x to stdout (format CSV, force_not_null(a));
 ERROR:  COPY FORCE_NOT_NULL cannot be used with COPY TO
 COPY x to stdout (format CSV, force_not_null *);
@@ -116,6 +140,10 @@ COPY x from stdin (format TEXT, force_null(a));
 ERROR:  COPY FORCE_NULL requires CSV mode
 COPY x from stdin (format TEXT, force_null *);
 ERROR:  COPY FORCE_NULL requires CSV mode
+COPY x from stdin (format RAW, force_null(a));
+ERROR:  COPY FORCE_NULL requires CSV mode
+COPY x from stdin (format RAW, force_null *);
+ERROR:  COPY FORCE_NULL requires CSV mode
 COPY x to stdout (format CSV, force_null(a));
 ERROR:  COPY FORCE_NULL cannot be used with COPY TO
 COPY x to stdout (format CSV, force_null *);
@@ -858,9 +886,11 @@ select id, text_value, ts_value from copy_default;
 (2 rows)
 
 truncate copy_default;
--- DEFAULT cannot be used in binary mode
+-- DEFAULT cannot be used in binary or raw mode
 copy copy_default from stdin with (format binary, default '\D');
 ERROR:  cannot specify DEFAULT in BINARY mode
+copy copy_default from stdin with (format raw, default '\D');
+ERROR:  cannot specify DEFAULT in RAW mode
 -- DEFAULT cannot be new line nor carriage return
 copy copy_default from stdin with (default E'\n');
 ERROR:  COPY default representation cannot use newline or carriage return
@@ -929,3 +959,19 @@ truncate copy_default;
 -- DEFAULT cannot be used in COPY TO
 copy (select 1 as test) TO stdout with (default '\D');
 ERROR:  COPY DEFAULT cannot be used with COPY TO
+--
+-- Test COPY FORMAT errors
+--
+\getenv abs_srcdir PG_ABS_SRCDIR
+\getenv abs_builddir PG_ABS_BUILDDIR
+\set filename :abs_builddir '/results/copy_raw_test_errors.data'
+-- Test single column requirement
+CREATE TABLE copy_raw_test_errors (col1 text, col2 text);
+COPY copy_raw_test_errors TO :'filename' (FORMAT raw);
+ERROR:  COPY with format 'raw' must specify exactly one column
+COPY copy_raw_test_errors (col1, col2) TO :'filename' (FORMAT raw);
+ERROR:  COPY with format 'raw' must specify exactly one column
+COPY copy_raw_test_errors FROM :'filename' (FORMAT raw);
+ERROR:  COPY with format 'raw' must specify exactly one column
+COPY copy_raw_test_errors (col1, col2) FROM :'filename' (FORMAT raw);
+ERROR:  COPY with format 'raw' must specify exactly one column
diff --git a/src/test/regress/sql/copy.sql b/src/test/regress/sql/copy.sql
index f1699b66b0..93595037dc 100644
--- a/src/test/regress/sql/copy.sql
+++ b/src/test/regress/sql/copy.sql
@@ -348,3 +348,27 @@ COPY parted_si(id, data) FROM :'filename';
 SELECT tableoid::regclass, id % 2 = 0 is_even, count(*) from parted_si GROUP BY 1, 2 ORDER BY 1;
 
 DROP TABLE parted_si;
+
+-- Test COPY FORMAT raw
+\set filename :abs_srcdir '/data/emp.data'
+CREATE TABLE copy_raw_test (col text);
+COPY copy_raw_test FROM :'filename' (FORMAT raw);
+SELECT col FROM copy_raw_test;
+TRUNCATE copy_raw_test;
+COPY copy_raw_test FROM :'filename' (FORMAT raw, DELIMITER E'\n');
+SELECT col FROM copy_raw_test ORDER BY col COLLATE "C";
+COPY copy_raw_test TO stdout (FORMAT raw, DELIMITER E'\n***\n');
+\qecho
+TRUNCATE copy_raw_test;
+COPY copy_raw_test FROM stdin (FORMAT raw, DELIMITER E'\n***\n');
+abc\.
+***
+"def",
+***
+
+***
+ghi
+***
+\.
+SELECT col FROM copy_raw_test ORDER BY col COLLATE "C";
+COPY copy_raw_test TO stdout (FORMAT raw, DELIMITER E'\n***\n');
diff --git a/src/test/regress/sql/copy2.sql b/src/test/regress/sql/copy2.sql
index 45273557ce..7aee4ca8ea 100644
--- a/src/test/regress/sql/copy2.sql
+++ b/src/test/regress/sql/copy2.sql
@@ -72,18 +72,32 @@ COPY x from stdin (log_verbosity default, log_verbosity verbose);
 -- incorrect options
 COPY x from stdin (format BINARY, delimiter ',');
 COPY x from stdin (format BINARY, null 'x');
+COPY x from stdin (format RAW, null 'x');
+COPY x from stdin (format TEXT, escape 'x');
+COPY x from stdin (format BINARY, escape 'x');
+COPY x from stdin (format RAW, escape 'x');
+COPY x from stdin (format TEXT, quote 'x');
+COPY x from stdin (format BINARY, quote 'x');
+COPY x from stdin (format RAW, quote 'x');
+COPY x from stdin (format RAW, header);
 COPY x from stdin (format BINARY, on_error ignore);
 COPY x from stdin (on_error unsupported);
-COPY x from stdin (format TEXT, force_quote(a));
-COPY x from stdin (format TEXT, force_quote *);
+COPY x to stdout (format TEXT, force_quote(a));
+COPY x to stdout (format TEXT, force_quote *);
+COPY x to stdout (format RAW, force_quote(a));
+COPY x to stdout (format RAW, force_quote *);
 COPY x from stdin (format CSV, force_quote(a));
 COPY x from stdin (format CSV, force_quote *);
 COPY x from stdin (format TEXT, force_not_null(a));
 COPY x from stdin (format TEXT, force_not_null *);
+COPY x from stdin (format RAW, force_not_null(a));
+COPY x from stdin (format RAW, force_not_null *);
 COPY x to stdout (format CSV, force_not_null(a));
 COPY x to stdout (format CSV, force_not_null *);
 COPY x from stdin (format TEXT, force_null(a));
 COPY x from stdin (format TEXT, force_null *);
+COPY x from stdin (format RAW, force_null(a));
+COPY x from stdin (format RAW, force_null *);
 COPY x to stdout (format CSV, force_null(a));
 COPY x to stdout (format CSV, force_null *);
 COPY x to stdout (format BINARY, on_error unsupported);
@@ -636,8 +650,9 @@ select id, text_value, ts_value from copy_default;
 
 truncate copy_default;
 
--- DEFAULT cannot be used in binary mode
+-- DEFAULT cannot be used in binary or raw mode
 copy copy_default from stdin with (format binary, default '\D');
+copy copy_default from stdin with (format raw, default '\D');
 
 -- DEFAULT cannot be new line nor carriage return
 copy copy_default from stdin with (default E'\n');
@@ -707,3 +722,19 @@ truncate copy_default;
 
 -- DEFAULT cannot be used in COPY TO
 copy (select 1 as test) TO stdout with (default '\D');
+
+--
+-- Test COPY FORMAT errors
+--
+
+\getenv abs_srcdir PG_ABS_SRCDIR
+\getenv abs_builddir PG_ABS_BUILDDIR
+
+\set filename :abs_builddir '/results/copy_raw_test_errors.data'
+
+-- Test single column requirement
+CREATE TABLE copy_raw_test_errors (col1 text, col2 text);
+COPY copy_raw_test_errors TO :'filename' (FORMAT raw);
+COPY copy_raw_test_errors (col1, col2) TO :'filename' (FORMAT raw);
+COPY copy_raw_test_errors FROM :'filename' (FORMAT raw);
+COPY copy_raw_test_errors (col1, col2) FROM :'filename' (FORMAT raw);
-- 
2.45.1

